Les librairies de composants résolvent un vrai problème : livrer une UI cohérente rapidement. Mais elles en créent un autre. Dès qu’il faut modifier un hover state, changer une animation ou surcharger un layout interne, on se bat contre l’abstraction au lieu d’écrire du CSS. shadcn/ui prend une approche différente. Ce n’est pas un package qu’on installe. C’est une collection de composants qu’on copie dans son projet, construits sur les primitives Radix UI et Tailwind CSS, qu’on possède et modifie librement.
Des composants qu’on possède vraiment
La CLI shadcn scaffolde les composants directement dans le codebase. Pas de dépendance node_modules à mettre à jour, pas de breaking changes à surveiller, pas de version pinning. Le composant vit dans le repo comme un fichier TypeScript ordinaire.
npx shadcn@latest add button dialog tabs
Ça génère des fichiers dans le répertoire components/ui/. Chaque composant est un wrapper léger autour d’une primitive Radix avec des classes Tailwind appliquées. Un Button fait ~30 lignes. Un Dialog ~50. Rien n’est caché.
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent',
ghost: 'hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
},
},
defaultVariants: { variant: 'default', size: 'default' },
}
);
cva (class-variance-authority) gère la logique de variants proprement. Ajouter un nouveau variant ou modifier un existant revient à éditer un objet JavaScript.
Theming avec les variables CSS
shadcn/ui utilise des variables CSS pour le theming au lieu de l’approche par config de Tailwind. Ça rend le changement de thème à runtime trivial et garde le design system cohérent à travers tous les composants.
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--muted: 210 40% 96.1%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
}
Chaque composant référence ces variables via des classes Tailwind comme bg-primary et text-muted-foreground. Changer toute la palette de couleurs revient à modifier quelques lignes de CSS, pas à fouiller dans des dizaines de fichiers de composants.
L’accessibilité intégrée
Radix UI gère les aspects complexes des composants accessibles : gestion du focus, navigation clavier, attributs ARIA et annonces pour les lecteurs d’écran. Un Dialog shadcn piège le focus correctement, se ferme avec Escape, et annonce son contenu aux technologies d’assistance. Un Select supporte la navigation clavier complète avec les flèches, la recherche par frappe et la sémantique listbox.
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Rechercher..." />
<CommandList>
<CommandGroup heading="Pages">
<CommandItem onSelect={() => navigate('/about')}>À propos</CommandItem>
<CommandItem onSelect={() => navigate('/blog')}>Blog</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
Cette palette de commandes est entièrement accessible au clavier dès la sortie de la boîte. Le focus est piégé dans le dialog, les éléments sont navigables avec les flèches, et le champ de recherche filtre les résultats en temps réel. Construire ça from scratch avec une accessibilité correcte prendrait des jours.
Composer des interfaces complexes
Là où shadcn/ui brille, c’est la composition. Comme chaque composant est un composant React classique avec des classes Tailwind, les combiner est naturel. Un data table avec tri, filtrage et pagination se construit en composant Table, Input, Select et Button qui partagent tous les mêmes design tokens.
<div className="flex items-center gap-2 py-4">
<Input
placeholder="Filtrer par nom..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="max-w-sm"
/>
<Select onValueChange={setStatus}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Statut" />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">Actif</SelectItem>
<SelectItem value="archived">Archivé</SelectItem>
</SelectContent>
</Select>
</div>
Pas de prop drilling à travers un DataTable monolithique. Pas de render props. Pas de slot APIs. Juste des composants composés comme React l’a prévu.
Conclusion
shadcn/ui fonctionne parce qu’il supprime la couche d’abstraction entre le design et le code. On obtient des composants accessibles et bien designés comme point de départ, puis on possède chaque ligne à partir de là. Pour les projets React utilisant Tailwind CSS, c’est le chemin le plus rapide vers une interface soignée qui ne fait pas générique et qui n’enferme pas dans l’API de quelqu’un d’autre.