Styliser l’UI SDK de Vercel avec Tailwind : data-slot, attributs d’état et animations d’entrée et de sortie
Stylisez le nouvel UI SDK de Vercel avec Tailwind v4 et les tokens shadcn : data-slot, attributs d’état, variables CSS et animations d’entrée et de sortie.
Preet Suthar · · 9 min de lecture
- React
- UI SDK
- Tailwind CSS
- shadcn/ui
Vercel Labs a publié UI SDK sur npm le 12 novembre 2026. C’est un ensemble de composants accessibles et sans style pour React, Vue, Svelte, Solid et Web Components, construits sur une bibliothèque centrale de comportements. Les composants gèrent le focus, le clavier et ARIA. Leur apparence, c’est votre travail.
Cet article en stylise quatre avec Tailwind CSS v4 et des tokens de thème façon shadcn : un Select, un Popover, un Rating et un Color Picker. Chacun montre un hook de style différent. Chaque extrait ci-dessous tourne dans une app de test construite avec Vite 8, React 19.3, Tailwind CSS 4.3 et @ui-sdk/react et @ui-sdk/core en 0.1.0-beta.1. UI SDK est en bêta, et sa doc indique que les API peuvent encore changer, donc vérifiez la version que vous installez.
Comment fonctionne le style dans UI SDK
UI SDK a trois couches. @ui-sdk/core contient des comportements comme le roving focus, le typeahead et les popups. Une primitive, écrite avec definePrimitive, combine des comportements avec un état et des slots nommés. Un adaptateur de framework comme @ui-sdk/react rend chaque slot sous forme de partie à importer, comme SelectTrigger ou RatingItem.
- Comportements
@ui-sdk/coreFocus, touches, sélection, popups, glisser-déposer. - Primitive
definePrimitiveL’état, une API et des slots nommés dans une seule définition. - Parties
@ui-sdk/reactChaque slot est rendu avecdata-slot, des attributs d’état et des variables CSS. - Vos styles
classNameDes utilitaires Tailwind ou du CSS pur qui ciblent ces hooks.
<div data-slot="select-item" data-highlighted data-selected role="option">
className="data-highlighted:bg-accent data-selected:font-medium"
Vos styles s’attachent aux parties par trois hooks :
data-slot: chaque partie en a un, commedata-slot="select-item". Il nomme la partie pour le CSS global et pour les agents.- Attributs d’état :
data-open,data-highlighted,data-selected,data-disabled, etc. La plupart sont présents ou absents. Certains portent une valeur, commedata-side="top"oudata-orientation="vertical". - Variables CSS : des valeurs qu’une classe ne peut pas exprimer, comme
--anchor-widthsur un popup ou--rating-fillsur une étoile.
className et style peuvent aussi être des fonctions de l’état d’une partie, comme Base UI le permet. Avec Tailwind, c’est rarement nécessaire, car v4 a une variante pour tout attribut data : data-highlighted:bg-accent s’applique tant que data-highlighted est présent.
La doc demande de styliser ces attributs plutôt que des ID générés, qui peuvent changer. Chaque page de composant a une section Styling et une section Data Attributes qui listent exactement ce que chaque partie expose. Cette liste est le contrat.
Configurer Tailwind v4 et la layer ui-sdk
UI SDK a besoin d’un peu de CSS à lui, par exemple pour garder un popup fermé caché. Il ajoute ce CSS à l’exécution dans une cascade layer nommée ui-sdk. Déclarez la layer en premier dans votre feuille de style globale, au-dessus de l’import Tailwind, pour que vos styles l’emportent quel que soit l’endroit où la page insère le CSS d’exécution :
La liste complète des tokens est celle que génère shadcn/ui, avec un bloc .dark qui redéfinit les mêmes noms. Je l’ai raccourcie ici. Rien dans UI SDK ne lit ces tokens. Ils comptent seulement parce que les classes ci-dessous les utilisent, ce qui veut dire qu’un composant UI SDK et un composant shadcn/ui peuvent partager un même thème.
Il n’y a pas de provider à monter ni de feuille de style à importer depuis le package. Si vous utilisez une Content Security Policy sans 'unsafe-inline' pour les styles, lisez la section CSP du guide Styling and Animation, car le CSS d’exécution a besoin d’un nonce ou de feuilles de style construites.
Select : attributs d’état et liste animée
Un Select a un label, un trigger, une valeur, un popup de contenu et des items. Celui-ci montre un sélecteur de rôle avec une coche sur l’item choisi et un court effet d’échelle et de fondu quand la liste s’ouvre et se ferme :

Ce que lit chaque classe :
data-open:bg-accentgarde le trigger teinté tant que la liste est ouverte.data-placeholder:stylise le trigger tant que rien n’est sélectionné. Avec une valeur par défaut, il ne s’applique jamais ici, mais le garder ne coûte rien.min-w-(--anchor-width)rend la liste au moins aussi large que le trigger. Tailwind v4 écrit une valeur de variable CSS sous la forme(--name), sansvar().origin-(--transform-origin)fait grandir la liste depuis le côté où elle s’ouvre. UI SDK définit cette variable sur le contenu.data-highlighted:marque l’item sous le pointeur ou sous le focus clavier. Les items sélectionnés reçoiventdata-selected, etSelectItemIndicatorn’est rendu que dans un item sélectionné.
Passer items est facultatif, mais la doc le recommande : le trigger peut afficher le label sélectionné avant le rendu de la liste, et le pont agent peut lire les choix pendant que la liste est fermée.
Si vous devez distinguer la surbrillance clavier de la surbrillance pointeur, le contenu porte data-modality="keyboard" ou "pointer". Un anneau de focus visible seulement pour les utilisateurs du clavier s’écrit in-data-[modality=keyboard]:data-highlighted:ring-2 sur l’item.
Le cycle de vie du popup
L’animation ci-dessus utilise deux attributs qui n’existent que pendant une transition. Le contenu reçoit data-starting-style pour sa première frame, pour qu’une transition ait un état de départ. À la fermeture, il reçoit data-ending-style et reste monté jusqu’à la fin de la transition ou de l’animation.
- FerméAbsent de la page
- Première frame
data-opendata-starting-styleStylisez l’état depuis lequel vous animez - Ouvert
data-openPasse en transition vers ses styles normaux - Fermeture
data-closeddata-ending-styleReste monté jusqu’à la fin de la sortie - DémontéOu masqué, avec keepMounted
Trois détails font que ça fonctionne en pratique.
D’abord, mettez les styles de sortie sur data-ending-style, pas sur data-closed. data-closed décrit l’état, mais la doc est explicite : il ne garde pas l’élément en place pour une sortie.
Ensuite, les popups d’UI SDK utilisent la top layer du navigateur. Le contenu est rendu sur place, comme enfant du composant, et le navigateur le place au-dessus de la page. Dans mon app de test, le contenu ouvert portait popover="manual" et correspondait à :popover-open. Il n’y a pas de partie Portal ni Positioner, et ce n’est pas le z-index qui décide s’il passe au-dessus d’un header sticky.
Enfin, si une bibliothèque d’animation comme Motion a besoin que le contenu fermé reste dans le DOM, activez keepMounted. onOpenChangeComplete s’exécute après la fin de l’ouverture ou de la fermeture, animation comprise.
Popover : une sortie plus rapide que l’entrée
Les entrées et les sorties n’ont pas besoin du même timing. Un popover qui glisse en 200 ms et disparaît en fondu en 100 ms paraît plus rapide qu’un popover qui prend 200 ms dans les deux sens. Comme la sortie a son propre attribut, Tailwind peut lui donner sa propre durée :

L’entrée passe de -translate-y-1, scale-95 et opacity-0 à l’état de repos. La sortie se limite à une réduction d’échelle et un fondu, avec data-ending-style:duration-100. motion-reduce:transition-none désactive les deux pour les personnes qui demandent moins de mouvement, et le popover s’ouvre et se ferme quand même.
PopoverTitle et PopoverDescription comptent au-delà de l’apparence. Le contenu est un dialog, et ces parties lui donnent un nom et une description accessibles. Sans titre, donnez au contenu un aria-label.
Rating : stylisez le remplissage, pas les étoiles
Un Rating est un groupe radio dessiné en étoiles. Vous rendez un seul RatingItem et le Rating le répète pour chaque étoile. Chaque item indique dans --rating-fill, de 0 à 1, quelle part de son étoile la note affichée couvre. data-filled marque les étoiles pleines, data-partial marque l’étoile où se termine une demi-note, et data-highlighted marque les étoiles jusqu’à la note que le pointeur prévisualise.
--rating-fill permet d’obtenir des demi-étoiles à partir d’un seul dégradé au lieu de deux icônes :

Avec step={0.5}, chaque demi-étoile est son propre radio, nommé « 3.5 stars », etc. Les radios sont des boutons que le Rating place au-dessus de votre icône avec des styles inline, et l’item reçoit position: relative de la même façon. C’est pour ça que le span peut utiliser absolute inset-0 sans classe supplémentaire.
Le clavier fonctionne sans aucun travail de votre part. Dans mon test, mettre le focus sur l’étoile cochée et appuyer sur la flèche droite a fait passer la note de 3,5 à 4, et le remplissage a suivi.
Gardez le masque hors de l’item focusable
Ma première version mettait le masque d’étoile sur RatingItem lui-même. Le rendu semblait correct jusqu’à ce que j’y arrive avec Tab : un masque CSS coupe tout ce que l’élément peint, contours compris, donc l’anneau de focus disparaissait. Déplacer le masque sur un span interne a réglé le problème. L’item garde son contour arrondi grâce à has-focus-visible:, et [&_button]:outline-none masque l’anneau par défaut du navigateur sur le bouton radio d’une demi-largeur à l’intérieur, qui dessinerait sinon un fin cadre autour d’une demi-étoile.

Ce n’est pas propre à UI SDK, mais les masques en forme d’icône sont une façon courante de dessiner des notes, donc vérifiez au clavier avant de livrer.
Color Picker : les variables CSS font le dessin
Le Color Picker expose le plus de variables des quatre. Chaque partie écrit la couleur courante dans --color-picker-color. La zone reçoit --color-area-background, un ensemble de dégradés pour ses deux canaux, et chaque slider reçoit --color-slider-background. Vos classes donnent une taille aux parties et utilisent ces variables comme arrière-plans.

Ce qu’il faut savoir avant de copier ceci :
- Le Popover se place dans le ColorPicker. Les parties du popup et la pastille du trigger modifient et affichent alors la même couleur.
bg-(image:--color-area-background)est l’indication de type de Tailwind v4. Sansimage:, Tailwind lirait la variable comme une couleur, et les dégradés ne s’afficheraient pas.- Les poignées reçoivent
position: absoluteet leur position en pourcentage en styles inline. Votre classe n’a qu’à centrer chaque poignée sur son point, ce que fait-translate-1/2. La poignée du slider a aussi besoin detop-1/2, car son style inline ne fixe que la position le long de la piste. - La pastille est une image pour les lecteurs d’écran, nommée d’après la couleur. Dans ma version, elle était annoncée « vivid purple » pour
#6366f1. Nommez le trigger vous-même, car le texte visible seul ne dit pas forcément à quoi sert la couleur. - Pour OKLCH, ajoutez
ColorPickerAreaCanvasdans la zone. Un dégradé ne peut pas dessiner le bord de la gamme OKLCH, donc le canvas la peint pixel par pixel.
Styliser par data-slot en CSS pur
Des classes utilitaires sur chaque partie sont une option. L’autre est une feuille de style basée sur data-slot, qui garde le markup court et regroupe l’apparence d’un composant au même endroit :
La doc d’UI SDK stylise aussi ses propres démos en CSS pur. Ça marche parce que data-slot fait partie des attributs qu’une partie garde toujours : la doc le liste à côté des rôles et de tabindex parmi les attributs gérés que vos props ne peuvent pas écraser. Dans un projet façon shadcn/ui, où chaque fichier de composant enveloppe les parties et ajoute ses propres classes, des utilitaires sur les parties sont en général plus simples. Un thème en CSS pur est plus facile à remplacer d’un bloc.
Migrer des classes façon shadcn depuis Radix ou Base UI
Si vos composants viennent de shadcn/ui, la plupart de vos classes se reportent telles quelles. Les différences tiennent à quelques noms d’attributs et à la structure des popups.
| Point | Radix | Base UI | UI SDK |
|---|---|---|---|
| État ouvert | data-[state=open]: | data-open: | data-open: |
| Styles d’entrée | keyframes sur data-[state=open] | data-starting-style: | data-starting-style: |
| Styles de sortie | keyframes sur data-[state=closed] | data-ending-style: | data-ending-style: |
| Origine de transformation | --radix-popover-content-transform-origin | --transform-origin | --transform-origin |
| Largeur du trigger | --radix-popover-trigger-width | --anchor-width | --anchor-width |
| Parties du popup | Portal, Content | Portal, Positioner, Popup | Content uniquement |
En venant de Base UI, les noms d’attributs des popups sont les mêmes, donc les chaînes de classes se reprennent presque telles quelles. Vous supprimez les wrappers Portal et Positioner et déplacez les props de placement comme side, align et sideOffset sur le contenu. En venant de Radix, vous remplacez les variantes data-[state=…] et les animations keyframes par les attributs de début et de fin. Dans les deux cas, vérifiez la liste Data Attributes de chaque composant, car des noms qui semblent partagés peuvent différer. Le Select de Base UI, par exemple, a son propre jeu d’attributs, et celui d’UI SDK aussi.
La comparaison des trois bibliothèques dans leur ensemble, au-delà du style, se trouve dans UI SDK vs Base UI vs Radix.
Ce qui m’a piégé
Une courte liste, pour que vous ne refassiez pas les mêmes erreurs :
- Styliser
data-closedpour une sortie. Rien ne s’anime. Utilisezdata-ending-style. - Oublier
@layer ui-sdk;. Sans cette ligne en premier, le CSS d’exécution peut arriver après vos styles dans la cascade. - Masques sur des parties focusables. Ils coupent le contour de focus. Masquez un élément interne.
- Lire
--available-heightdepuis un enfant. La doc précise que la variable n’est pas héritée. Copiez-la sur le popup, par exemple[--list-height:var(--available-height)], et lisezmax-h-(--list-height)plus bas. - Dégradés depuis des variables. Utilisez
bg-(image:--name), pasbg-(--name).
UI SDK est une bêta, vieille d’un jour au moment où j’écris. Tout ici a fonctionné comme documenté en 0.1.0-beta.1, et les listes d’attributs de la doc embarquée dans node_modules/@ui-sdk/react/docs/ correspondent à ce que les composants ont rendu. HextaUI prépare une version stylisée UI SDK de ses composants, distincte de la version Base UI, et cet article rassemble ce que j’ai appris en construisant les premières pièces.




