Blog

UI SDK vs Base UI vs Radix : quelle bibliothèque headless pour des composants façon shadcn ?

UI SDK de Vercel, Base UI et Radix comparés pour des composants façon shadcn : modèle, frameworks, couverture, style, animation, bêta, licence et agents.

Preet Suthar · · 9 min de lecture

  • React
  • UI SDK
  • Base UI
  • Radix

Jusqu’à cette semaine, choisir la couche de primitives sous une bibliothèque de composants façon shadcn/ui voulait dire choisir entre deux options : Radix et Base UI. Le 12 novembre 2026, Vercel Labs en a publié une troisième, UI SDK, en 0.1.0-beta.1. Il est headless comme les deux autres, mais il repose sur une autre idée, et il fournit quelque chose qu’aucun des deux n’a : un pont qui permet à des agents IA de lire et de piloter les composants.

Une précision avant la comparaison. Les composants de HextaUI sont construits sur Base UI aujourd’hui, et j’ajoute une version UI SDK séparée à côté. J’ai des raisons d’apprécier les deux, donc j’ai essayé de rester équitable, et chaque affirmation ci-dessous vient de la doc des bibliothèques, de leurs métadonnées npm, ou d’une app de test où j’ai construit le même composant trois fois.

Si vous voulez la migration de Radix vers Base UI en détail, avec chaque changement d’API dans le code shadcn/ui, lisez d’abord shadcn/ui est passé à Base UI. Cet article ne la répète pas. Il ajoute UI SDK au tableau et se demande de laquelle partir.

La réponse courte

  • Choisissez Radix si vous avez une base de code Radix qui fonctionne, que votre équipe la connaît et que vous n’avez pas besoin d’un composant qui lui manque. Il est stable, React uniquement et sous MIT.
  • Choisissez Base UI pour un nouveau design system React que vous voulez livrer sur des API stables ce trimestre. C’est la base par défaut des nouveaux projets shadcn/ui, il couvre plus de composants que Radix et utilise le modèle d’attributs vers lequel se dirige la majeure partie de l’écosystème.
  • Choisissez UI SDK si vous avez besoin de plus que React, si vous voulez des composants comme Color Picker, Date Picker ou Tree sans seconde bibliothèque, ou si vous voulez que des agents IA pilotent votre UI via des outils structurés. Acceptez que ce soit une bêta.

La suite de l’article apporte les preuves.

Les trois en un coup d’œil

RadixBase UIUI SDK
Packageradix-ui 1.8.1@base-ui/react 1.9.0@ui-sdk/react 0.1.0-beta.1
LicenceMITMITApache-2.0
StatutStableStableBêta
FrameworksReactReactReact, Vue, Svelte, Solid, Web Components
État ouvertdata-state="open"data-opendata-open
Entrée et sortieKeyframes sur data-statedata-starting-style, data-ending-styledata-starting-style, data-ending-style
data-slot sur les partiesNonNonOui
Parties du popupPortal, ContentPortal, Positioner, PopupContenu dans la top layer
Pont agentNonNonOui, avec WebMCP
Radix, Base UI et UI SDK en un coup d’œil, novembre 2026

Les trois sont sans style et gèrent le focus, le clavier et ARIA pour vous. Les versions comparées sont les plus récentes sur npm au 13 novembre 2026 : radix-ui 1.8.1 (publiée le 12 octobre 2026), @base-ui/react 1.9.0 (9 octobre 2026) et @ui-sdk/react 0.1.0-beta.1 (12 novembre 2026).

Trois modèles différents

Radix livre une primitive par composant. Un package Dialog vous donne Root, Trigger, Portal, Overlay, Content, Title, Description et Close, et c’est l’unité avec laquelle vous travaillez. Les comportements internes, comme le piège de focus et la fermeture, ne sont pas exposés. Le package radix-ui les regroupe tous.

Base UI a la même forme : un composant, plusieurs parties. Il ajoute davantage d’utilitaires publics autour, comme useRender et mergeProps, pour que vous puissiez écrire vos propres parties qui se comportent comme celles intégrées. Il est React uniquement, et construit par des personnes qui ont travaillé sur Radix, Material UI et Floating UI.

UI SDK découpe les couches autrement. @ui-sdk/core est une bibliothèque TypeScript de comportements : roving focus, typeahead, popups, glisser-déposer, annonces et bien d’autres. Une primitive combine des comportements avec un état et des slots nommés via definePrimitive. Les composants que vous importez depuis @ui-sdk/react sont des primitives rendues par l’adaptateur React, et les mêmes primitives sont rendues dans Vue, Svelte, Solid et Web Components par leurs adaptateurs. Sa doc le présente pour les agents : donnez à un agent des comportements testés et il peut construire un composant que la bibliothèque ne fournit pas, comme un sélecteur d’icônes ou un tableau kanban.

La différence en pratique : dans Radix et Base UI, un composant dont vous avez besoin et qu’ils n’ont pas est à construire de zéro. Dans UI SDK, vous le construisez avec les mêmes comportements que les composants intégrés, et il peut rejoindre le pont agent.

Même popover, trois anatomies

J’ai construit un même popover trois fois dans une app Vite avec les mêmes classes Tailwind. Voici les arbres, réduits aux parties qui diffèrent :

import { Popover as BasePopover } from "@base-ui/react/popover"
import {
  Popover,
  PopoverContent,
  PopoverTitle,
  PopoverTrigger,
} from "@ui-sdk/react/popover"
import { Popover as RadixPopover } from "radix-ui"

function RadixVersion() {
  return (
    <RadixPopover.Root>
      <RadixPopover.Trigger className={trigger}>Radix</RadixPopover.Trigger>
      <RadixPopover.Portal>
        <RadixPopover.Content
          align="start"
          sideOffset={8}
          className={`${panel} origin-(--radix-popover-content-transform-origin) data-[state=open]:animate-[popover-in_150ms_ease-out] data-[state=closed]:animate-[popover-out_100ms_ease-in]`}
        >
          <p className="font-medium">Share project</p>
        </RadixPopover.Content>
      </RadixPopover.Portal>
    </RadixPopover.Root>
  )
}

function BaseUIVersion() {
  return (
    <BasePopover.Root>
      <BasePopover.Trigger className={trigger}>Base UI</BasePopover.Trigger>
      <BasePopover.Portal>
        <BasePopover.Positioner align="start" sideOffset={8}>
          <BasePopover.Popup
            className={`${panel} origin-(--transform-origin) transition-[opacity,scale] duration-150 data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:scale-95 data-ending-style:opacity-0`}
          >
            <BasePopover.Title className="font-medium">
              Share project
            </BasePopover.Title>
          </BasePopover.Popup>
        </BasePopover.Positioner>
      </BasePopover.Portal>
    </BasePopover.Root>
  )
}

function UISDKVersion() {
  return (
    <Popover>
      <PopoverTrigger className={trigger}>UI SDK</PopoverTrigger>
      <PopoverContent
        align="start"
        sideOffset={8}
        className={`${panel} origin-(--transform-origin) transition-[opacity,scale] duration-150 data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:scale-95 data-ending-style:opacity-0`}
      >
        <PopoverTitle className="font-medium">Share project</PopoverTitle>
      </PopoverContent>
    </Popover>
  )
}

trigger et panel sont des chaînes de classes partagées, et la version Radix utilise deux petites règles @keyframes pour son entrée et sa sortie. Le résultat est identique dans les trois :

Trois popovers ouverts empilés, sous des triggers nommés Radix, Base UI et UI SDK, chacun affichant Share project
Le même popover construit avec Radix, Base UI et UI SDK, capturé depuis l’app de test. Les pixels sont identiques. Le DOM et le code, non.

Radix

  • Popover.Root
  • Popover.Trigger
  • Popover.Portal
  • Popover.Content

Base UI

  • Popover.Root
  • Popover.Trigger
  • Popover.Portal
  • Popover.Positioner
  • Popover.Popup

UI SDK

  • Popover
  • PopoverTrigger
  • PopoverContentRendu sur place, dans la top layer

Une partie qui sert à déplacer ou placer le popup

Le même popover, trois arbres de parties

Ce qui diffère, c’est l’endroit où vit le contenu. Dans mon test, Radix et Base UI ont tous deux déplacé le contenu ouvert à la fin de <body> via leur Portal. UI SDK l’a laissé dans son propre composant, avec popover="manual", et le navigateur l’a promu dans la top layer. Cela a des conséquences :

  • Plus de conflits d’empilement. Le contenu en top layer passe au-dessus de la page, quel que soit le z-index d’un header sticky ou d’un parent transformé.
  • Les événements remontent dans votre arbre. La doc d’UI SDK prévient qu’un gestionnaire de clic ou un lien qui entoure un popup reçoit aussi les clics à l’intérieur. Gardez les popups hors des cartes cliquables.
  • Moins de parties wrapper. Les props de placement comme side, align et sideOffset vont sur le contenu, pas sur un Positioner.

Radix et Base UI gèrent bien les portals depuis des années, donc le premier point est moins une correction qu’une simplification. Le second est un nouveau comportement à connaître.

Hooks de style

Les trois placent l’état dans des attributs data, mais les noms diffèrent, et une seule nomme chaque partie.

HookRadixBase UIUI SDK
État ouvertdata-state="open"data-opendata-open
Nom de la partieaucunaucundata-slot sur chaque partie
Origine de transformation--radix-popover-content-transform-origin--transform-origin--transform-origin
Largeur de l’ancre--radix-popover-trigger-width--anchor-width--anchor-width
État dans `className`nonfonction de l’étatfonction de l’état

La ligne data-slot compte plus qu’il n’y paraît. shadcn/ui ajoute ses propres attributs data-slot dans ses fichiers wrapper, parce que Radix et Base UI n’en émettent pas. Dans mon app de test, le popup Base UI portait data-open, data-side et data-align, et aucun data-slot. UI SDK le place lui-même sur chaque partie, comme data-slot="popover-content", et la doc le traite comme un attribut géré que vos props ne peuvent pas écraser. Le CSS global et les agents ont ainsi un nom stable pour chaque partie.

Si vous stylisez avec Tailwind, passer de Base UI à UI SDK consiste surtout à supprimer des wrappers, car les noms d’attributs des popups sont partagés. Passer de Radix implique de remplacer les variantes data-[state=open]: et les variables préfixées par Radix. Le tutoriel de style UI SDK détaille quatre composants avec des tokens façon shadcn.

Animation

Radix anime le montage et le démontage avec des animations CSS sur data-state. Sa doc indique qu’il suspend le démontage pendant que l’animation de sortie joue. Pour les bibliothèques d’animation JavaScript, de nombreuses parties acceptent forceMount pour que la bibliothèque décide quand les enfants sont montés.

Base UI ajoute data-starting-style et data-ending-style, donc les transitions CSS fonctionnent dans les deux sens, pas seulement les keyframes. Le popup reste monté jusqu’à la fin de l’animation, puis il est retiré, ou masqué si le Portal a keepMounted. Son guide d’animation couvre Motion, et indique qu’il détecte les animations Motion qui incluent opacity.

UI SDK utilise les deux mêmes attributs avec le même sens. Le contenu reste monté jusqu’à la fin de sa transition ou animation de sortie, keepMounted garde le contenu fermé dans la page, et onOpenChangeComplete s’exécute quand l’ouverture ou la fermeture est terminée. La doc d’UI SDK n’a pas encore de guide pour Motion, à part une note qui conseille keepMounted quand une bibliothèque d’animation a besoin que le contenu fermé reste dans le DOM.

Pour une animation uniquement en CSS, Base UI et UI SDK se valent en pratique. Pour des interfaces qui reposent beaucoup sur Motion, Base UI a des patterns documentés aujourd’hui, UI SDK non.

Couverture des composants

Compter les composants d’une bibliothèque à l’autre reste approximatif, car chacune découpe et nomme les choses différemment. À titre indicatif :

  • Radix : 30 packages de primitives dans radix-ui 1.8.1, d’Accordion à Tooltip, plus des utilitaires comme Slot, Portal et Visually Hidden.
  • Base UI : 37 composants dans l’index de sa doc, dont Combobox, Autocomplete, Number Field, OTP Field, Drawer, Toast et Preview Card, que Radix n’a pas.
  • UI SDK : 55 composants et deux hooks dans la doc fournie avec 0.1.0-beta.1.

La plupart des composants Base UI ont un équivalent UI SDK, parfois sous un autre nom : Number Field devient Number Input, Preview Card devient Hover Card, OTP Field devient OTP Input. UI SDK fournit aussi des composants qu’aucune des deux autres n’a, dont Calendar, Date Picker, Date Input, Time Input, Color Picker, Rating, Tree, Command, Carousel, Resizable, Image Cropper, File Input, Token Input, Trigger Menu, Timeline et Waterfall.

Le nombre ne fait pas la qualité. Les composants Radix et Base UI ont des années d’utilisation en production derrière eux. Les pages de composants d’UI SDK listent leurs comportements testés ligne par ligne, ce qui est bon signe, mais un jour de vie publique ne vaut pas des années de rapports de bugs.

Le pont agent

C’est la partie que seul UI SDK possède. createAgent() de @ui-sdk/react/agent se connecte à chaque composant UI SDK de la page. snapshot() les liste avec leurs labels, leur état et les actions disponibles. act() appelle ces actions via l’API de chaque composant, et vos callbacks de changement voient details.source === "agent". registerTools() propose les mêmes outils à un agent intégré au navigateur via WebMCP.

Radix et Base UI n’ont rien de tel intégré. Un agent les pilote comme n’importe quelle page web : via des captures d’écran, le DOM ou l’arbre d’accessibilité, avec des outils de navigateur qui cliquent et tapent.

La doc de Vercel inclut un benchmark de cette différence sur une tâche de paramètres de projet. D’après leurs chiffres, avec Sonnet via Claude Code et des médianes sur trois exécutions, le pont agent a fait passer les appels d’outils de 16 à 5 et le coût de 0,073 $ à 0,032 $. Ce sont les chiffres de Vercel pour une tâche, pas les miens, mais la tendance est logique : lire une liste de composants avec leur état coûte moins cher que lire une page. J’ai construit une démo fonctionnelle du pont, avec des permissions et un composant personnalisé, dans Rendre votre app React utilisable par des agents IA.

Si des agents qui utilisent votre UI ne sont pas dans votre roadmap, cette section ne doit rien décider. S’ils le sont, rien d’autre dans cette comparaison n’en approche.

Maturité et licence

Radix et Base UI sont des bibliothèques stables en 1.x. Leurs API évoluent selon semver, et shadcn/ui publie chaque composant pour les deux.

UI SDK indique dans son README et sa doc qu’il est en bêta et que le framework, les API, la documentation et le comportement peuvent changer avant la disponibilité générale. Sa bêta est couverte par l’accord de bêta public de Vercel. Certaines API sont en plus marquées expérimentales, dont la clé agent sur les primitives personnalisées et les helpers d’historique et d’annulation. Le dépôt GitHub, vercel-labs/ui, a été créé en octobre 2026, et la seule entrée du changelog est « Introducing UI SDK », le 12 novembre 2026.

Les licences diffèrent aussi. Radix et Base UI sont sous MIT. UI SDK est sous Apache-2.0, une licence elle aussi permissive qui ajoute une concession explicite de brevets. Si vous le redistribuez, Apache-2.0 vous demande de conserver sa licence et ses mentions, donc vérifiez auprès de la personne qui gère les licences dans votre entreprise si c’est important pour vous.

Frameworks

Radix et Base UI sont des bibliothèques React. UI SDK publie @ui-sdk/react, @ui-sdk/vue, @ui-sdk/svelte, @ui-sdk/solid et @ui-sdk/web à la même version, tous au-dessus d’un même cœur. Si votre entreprise fait tourner une app React et une app Vue, ou veut une version Web Components pour un CMS, UI SDK est le seul des trois à toutes les couvrir avec un seul jeu de comportements et de noms d’attributs.

Le package React d’UI SDK exige React 19 et React DOM 19 ou plus récent, avec @types/react 19.2.10 ou plus récent. Si vous êtes encore sur React 18, la question est réglée pour l’instant.

Quand choisir chacun

Restez sur Radix si l’app fonctionne, que l’équipe le connaît et que rien ne vous bloque. Rien ici n’impose de migrer. Si vous migrez, passez d’abord à Base UI : c’est le plus petit pas, et le modèle d’attributs que vous apprenez se transpose à UI SDK.

Partez sur Base UI si vous construisez une bibliothèque de composants React ou une UI produit à livrer maintenant. Il est stable, c’est le choix par défaut de shadcn/ui, et ses patterns de popups, de formulaires et d’animation sont bien documentés.

Partez sur UI SDK si l’un de ces points est vrai :

  • vous avez besoin que plusieurs frameworks partagent un même modèle de composants ;
  • vous avez besoin de ses composants en plus et ajouteriez sinon des bibliothèques pour les dates, les couleurs ou les arbres ;
  • faire piloter votre UI par des agents est un objectif produit, et vous voulez des permissions et des sources de changement intégrées ;
  • vous prototypez et pouvez absorber des changements cassants pendant la bêta.

Mélanger est possible. Les composants UI SDK n’ont pas besoin de provider, donc une app Base UI peut utiliser un Color Picker UI SDK sur un écran. Vous portez deux dépendances et deux jeux de noms d’attributs, donc limitez-vous aux composants que vous ne trouvez pas ailleurs.

Ce que fait HextaUI

Les composants de HextaUI restent sur Base UI. C’est la version présente sur le site aujourd’hui, et elle ne va pas disparaître. Je construis aussi une version UI SDK séparée, avec le même look et les mêmes tokens de thème, pour les projets qui veulent le pont agent ou les composants en plus. Elle n’est pas encore sortie. Quand elle le sera, vous choisirez une version, sans migrer.