shadcn/ui : la bibliothèque de composants qui n'en est pas une
Guide complet et sourcé de shadcn/ui : philosophie du code ouvert, fonctionnement du CLI et du registre, stratégie de mises à jour avec diff, et comparaison avec les bibliothèques traditionnelles.
Les bibliothèques de composants React ont un problème structurel. On installe un paquet npm, on importe un <Button>, on le personnalise via des props ou un système de thème… et on finit par se battre contre l’abstraction dès qu’on veut sortir des sentiers battus. Override de styles, wrappers de contournement, !important en cascade : le composant qu’on ne possède pas finit toujours par résister.
shadcn/ui propose une réponse radicale à ce problème. Ce n’est pas un paquet npm. C’est une plateforme de distribution de code qui copie le source des composants directement dans votre projet. Vous possédez le code. Vous le modifiez librement. Il n’y a pas de node_modules/shadcn dans votre arbre de dépendances.
Avec plus de 110 000 étoiles sur GitHub et une adoption massive dans l’écosystème React, cette approche a clairement touché un nerf. Voyons comment ça fonctionne concrètement.
Source : shadcn-ui/ui — GitHub
1. L’architecture : primitives headless + styling + distribution
shadcn/ui repose sur trois couches distinctes :
┌──────────────────────────────────┐
│ Votre code (modifiable) │ ← Ce que shadcn copie chez vous
├──────────────────────────────────┤
│ Tailwind CSS (styling) │ ← Variables CSS, classes utilitaires
├──────────────────────────────────┤
│ Radix UI (comportement) │ ← Accessibilité, clavier, ARIA
└──────────────────────────────────┘
Radix UI est une bibliothèque de primitives headless : elle gère le comportement complexe des composants (navigation clavier, focus management, conformité WAI-ARIA, gestion d’état) sans imposer aucun style visuel. C’est une vraie dépendance npm, elle — installée dans node_modules.
Tailwind CSS fournit le système de design via des variables CSS et des classes utilitaires. Les tokens de design (couleurs, espacements, rayons) sont explicites dans le code, pas cachés derrière un objet de thème abstrait.
shadcn/ui assemble ces deux couches en composants prêts à l’emploi, puis les distribue sous forme de fichiers source. C’est cette dernière étape — la distribution — qui fait toute la différence.
Source : What are Radix Primitives? — Vercel Academy
2. Le CLI : comment ça fonctionne en pratique
Initialisation
npx shadcn@latest init
Cette commande configure le projet : elle crée un fichier components.json qui stocke les préférences (chemin CSS, aliases d’import, framework cible, choix des primitives Radix ou Base UI). On peut aussi partir de zéro avec un template :
npx shadcn@latest init --template vite --preset base-nova
Ajout d’un composant
npx shadcn@latest add button
Cette commande ne fait pas un npm install. Elle :
- Résout le composant dans le registre (par défaut
ui.shadcn.com) - Télécharge le fichier JSON décrivant le composant (source, dépendances, CSS)
- Copie les fichiers source dans votre projet (typiquement
components/ui/button.tsx) - Installe les dépendances npm nécessaires (comme
@radix-ui/react-slot) - Met à jour votre CSS si le composant introduit de nouvelles variables
Après exécution, vous avez un fichier button.tsx dans votre arborescence. C’est votre fichier. Vous pouvez l’ouvrir, le lire, le modifier, le supprimer. Aucune magie, aucune indirection.
Prévisualisation avant modification
npx shadcn@latest add button --dry-run # Simule sans écrire
npx shadcn@latest add button --diff # Affiche les différences
npx shadcn@latest add button --view # Montre le contenu des fichiers
Les flags --diff et --view impliquent automatiquement --dry-run.
Source : shadcn CLI Reference
3. “Copier-coller le code” : qu’est-ce que ça veut dire vraiment ?
L’expression “copier-coller” est trompeuse. Elle évoque un développeur qui va sur un site, sélectionne du code, fait Ctrl+C / Ctrl+V dans son éditeur. Ce n’est pas ce qui se passe.
Ce que ça veut dire
Le CLI automatise la copie du code source des composants dans votre projet. Concrètement, après npx shadcn@latest add dialog, vous avez un fichier comme celui-ci dans votre arborescence :
// components/ui/dialog.tsx — ce fichier est CHEZ VOUS
"use client";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogContent({
className,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
return (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay
className={cn("fixed inset-0 z-50 bg-black/50", className)}
/>
<DialogPrimitive.Content
className={cn(
"fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2",
"w-full max-w-lg rounded-lg border bg-background p-6 shadow-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4">
<X className="h-4 w-4" />
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
);
}
export { Dialog, DialogContent };
Le comportement complexe (gestion du focus, fermeture au clic extérieur, attributs ARIA) est délégué à @radix-ui/react-dialog, qui reste une dépendance npm classique. Mais toute la couche visuelle — les classes Tailwind, la structure JSX, la composition — est dans votre code.
Ce que ça change par rapport à une bibliothèque classique
| Aspect | Bibliothèque npm (MUI, Chakra) | shadcn/ui |
|---|---|---|
| Installation | npm install @mui/material | npx shadcn@latest add button |
| Code source | Dans node_modules, non modifiable | Dans votre projet, modifiable |
| Personnalisation | Props, thème, overrides CSS | Modification directe du fichier |
| Mise à jour | npm update automatique | Manuelle, via diff |
| Dépendance | Le paquet entier | Seulement les primitives utilisées |
| Bundle size | Tout le paquet (tree-shaking partiel) | Seulement ce que vous avez ajouté |
Le vrai sens de “copier-coller”
Il serait plus juste de parler de distribution de code source. shadcn/ui est un catalogue de composants dont le CLI automatise l’installation locale. Une fois installé, le composant n’a plus aucun lien avec shadcn — c’est votre code, point final.
Source : Why shadcn/ui is Different — Vercel Academy
4. Le système de registre
Le registre est le mécanisme qui rend tout ça possible. C’est un catalogue JSON qui décrit les composants disponibles, leurs dépendances et leurs fichiers.
Structure du registre
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "acme",
"homepage": "https://acme.com",
"items": [
{
"name": "hello-world",
"type": "registry:component",
"title": "Hello World",
"description": "A simple hello world component.",
"dependencies": ["@radix-ui/react-slot"],
"files": [
{
"path": "registry/new-york/hello-world/hello-world.tsx",
"type": "registry:component"
}
]
}
]
}
Quand vous exécutez npx shadcn@latest add hello-world, le CLI :
- Récupère le fichier JSON du composant depuis le registre
- Résout les dépendances (npm et autres composants du registre)
- Copie les fichiers en adaptant les chemins d’import à votre configuration
- Installe les dépendances npm manquantes
Créer son propre registre
C’est là que le modèle devient puissant pour les équipes. Vous pouvez créer un registre interne pour distribuer vos composants :
# 1. Définir vos composants dans registry.json
# 2. Builder le registre
npx shadcn@latest build
# Génère des fichiers JSON dans public/r/
# → public/r/hello-world.json
# → public/r/data-table.json
// package.json
{
"scripts": {
"registry:build": "shadcn build"
}
}
Les composants sont ensuite installables par URL :
npx shadcn@latest add https://acme.com/r/data-table.json
Registres nommés (namespaced)
Depuis le CLI v3 (août 2025), on peut préfixer les composants par registre :
npx shadcn@latest add @acme/data-table
npx shadcn@latest add @magicui/shimmer-button
npx shadcn@latest add @clerk/sign-in
Cela permet de mixer des composants de plusieurs sources — le registre officiel shadcn, un registre d’entreprise, des registres communautaires — dans un même projet.
Et sans Tailwind ?
La question revient souvent : peut-on utiliser le système de registre pour distribuer des composants qui n’utilisent pas Tailwind — avec des CSS Modules, par exemple ?
En théorie, oui. Le registre est un mécanisme de distribution de fichiers. Le schema registry-item.json supporte plusieurs types de fichiers (registry:component, registry:lib, registry:hook, registry:file…) et une propriété css qui accepte des règles CSS arbitraires (@layer, @keyframes, @plugin). Rien dans le protocole de distribution n’impose Tailwind.
Un registre custom pourrait parfaitement distribuer :
registry/
├── components/
│ ├── button/
│ │ ├── button.tsx ← type: registry:component
│ │ └── button.module.css ← type: registry:file
│ └── dialog/
│ ├── dialog.tsx
│ └── dialog.module.css
En pratique, c’est un autre projet. Le couplage Tailwind n’est pas dans le registre — il est dans le code des composants. Chaque composant officiel utilise des classes Tailwind directement dans le JSX (className="fixed inset-0 z-50 bg-black/50"). Créer un registre CSS Modules signifie réécrire chaque composant pour extraire les styles dans des fichiers .module.css séparés. C’est exactement ce qu’a fait le projet communautaire shadcn-css, qui propose une adaptation complète des composants en CSS Modules — un effort conséquent mais qui démontre que l’architecture le permet.
Le registre shadcn est donc un système de distribution agnostique qui distribue un catalogue de composants couplés à Tailwind. La distinction est importante : l’outil est flexible, le contenu par défaut ne l’est pas.
Source : registry-item.json — shadcn/ui
Source : Discussion #2832 — shadcn-ui variation without Tailwind
Source : Registry — Introduction — shadcn/ui
Source : Registry — Getting Started — shadcn/ui
5. Gérer les versions et les mises à jour
C’est la question légitime que tout le monde pose : si le code est copié dans mon projet, comment je gère les mises à jour ?
La commande diff
# Vérifier les mises à jour pour tous les composants
npx shadcn diff
# Vérifier un composant spécifique
npx shadcn diff button
Cette commande compare vos fichiers locaux avec la dernière version du registre et affiche un diff. Pas de mise à jour automatique — c’est vous qui décidez quoi appliquer.
Deux stratégies de maintenance
Stratégie 1 : Proxy / pré-composition
On garde les composants shadcn intacts et on crée une couche par-dessus :
// components/ui/button.tsx — composant shadcn original, non modifié
// components/app-button.tsx — votre wrapper
import { Button } from "@/components/ui/button";
export function AppButton({ children, ...props }) {
return (
<Button variant="outline" size="sm" {...props}>
{children}
</Button>
);
}
Avantage : on peut mettre à jour les composants shadcn librement avec --overwrite :
npx shadcn@latest add button --overwrite
npx shadcn@latest add --all --overwrite # Tout mettre à jour d'un coup
Stratégie 2 : Modification directe
On modifie les composants shadcn eux-mêmes. C’est plus simple au quotidien, mais les mises à jour demandent un merge manuel en s’appuyant sur le diff.
En pratique, la stratégie proxy est recommandée pour les projets d’équipe ou les design systems internes. Elle sépare clairement “ce qui vient de shadcn” de “ce qu’on a personnalisé”, ce qui simplifie les mises à jour.
Source : Updating and Maintaining Components — Vercel Academy
Ce qui n’existe pas (et c’est voulu)
Il n’y a pas de versioning automatique des composants, pas de shadcn update, pas de CHANGELOG par composant. C’est un choix délibéré : le moment où vous ajoutez un composant, il devient votre code. La relation avec le registre upstream est consultative, pas contraignante.
6. CLI v4 : l’outillage actuel (mars 2026)
La version 4 du CLI, sortie en mars 2026, apporte plusieurs évolutions significatives.
La commande info
npx shadcn@latest info
Affiche un état complet du projet : framework détecté, variables CSS, composants installés, liens vers la documentation de chaque composant. Particulièrement utile pour donner du contexte aux agents de code (Copilot, Claude, etc.).
Le système de presets
Un preset encode la configuration complète d’un design system en un code court :
npx shadcn@latest init --preset base-nova
npx shadcn@latest init --preset a2r6bw
Un preset contient : couleurs, thèmes, bibliothèque d’icônes, polices, rayons de bordure. On peut créer ses propres presets, les publier dans un registre, et les partager avec son équipe.
Les skills (pour agents IA)
Le CLI v4 introduit un système de “skills” qui donne aux agents de code le contexte nécessaire pour travailler avec les composants shadcn : primitives Radix, APIs mises à jour, patterns de composants, workflows de registre.
Types de registre élargis
registry:block— des pages ou sections complètes (pas seulement des composants unitaires)registry:base— un design system complet (composants, dépendances, CSS, polices, config)registry:font— distribution de polices comme des composants
Source : March 2026 — shadcn/cli v4 — shadcn/ui
7. Comparaison avec les bibliothèques traditionnelles
MUI (Material UI)
MUI installe un écosystème complet via npm : @mui/material, @mui/system, @emotion/react, @emotion/styled. La personnalisation passe par un objet de thème et un système de sx prop. C’est puissant mais opaque — quand on veut modifier le comportement interne d’un composant, on se heurte à l’abstraction.
Chakra UI
Chakra adopte une approche similaire à MUI (dépendance npm, thème centralisé) avec une API plus simple. L’accessibilité est excellente. Mais le même problème fondamental demeure : le code du composant est dans node_modules.
Ce que shadcn change
Le modèle shadcn transforme la relation développeur-bibliothèque :
- Bibliothèque npm : relation de dépendance. Le code est chez l’éditeur, vous consommez son API.
- shadcn/ui : relation de propriété. Le code est chez vous, vous en faites ce que vous voulez.
Concrètement, avec MUI, si vous voulez changer le comportement de fermeture d’un Dialog, vous devez espérer qu’une prop existe pour ça. Avec shadcn, vous ouvrez dialog.tsx et vous modifiez le code.
Le compromis est clair : vous gagnez en contrôle, vous perdez en confort de maintenance. Pas de npm update qui corrige un bug d’accessibilité automatiquement — c’est à vous de surveiller le diff et d’appliquer les corrections.
8. Au-delà de React : le modèle shadcn appliqué à d’autres stacks
Le principe “distribuer du code source plutôt qu’un paquet npm” n’a rien de spécifique à React. Naturellement, l’idée a essaimé.
Ports officiellement référencés
shadcn/ui maintient une liste de ports communautaires pour d’autres frameworks :
| Port | Framework | Approche |
|---|---|---|
| shadcn-vue | Vue | Radix Vue + Tailwind, même CLI |
| shadcn-svelte | Svelte | Bits UI (primitives) + Tailwind |
| spartan | Angular | Primitives Angular + Tailwind |
| solid-ui | Solid.js | Kobalte (primitives) + Tailwind |
Ces ports reprennent le même modèle : des primitives headless spécifiques au framework + Tailwind + un CLI qui copie le code. Le registre shadcn lui-même est framework-agnostique — il distribue des fichiers JSON qui décrivent des composants. Ce sont les composants qui ciblent un framework.
Source : Awesome shadcn/ui Framework Ports
Astro : un cas particulier
shadcn/ui supporte officiellement Astro comme cible d’installation (npx shadcn@latest init --template astro). Mais il faut comprendre ce que ça signifie : les composants installés restent des composants React utilisés dans Astro via les îlots d’interactivité. Ce n’est pas un port natif en composants .astro.
Il n’existe pas aujourd’hui de portage du catalogue shadcn en composants Astro natifs (fichiers .astro purs). C’est logique : les composants Astro n’ont pas de runtime côté client, ce qui rend difficile le portage des composants interactifs (Dialog, Dropdown, Tabs…) sans recourir à un framework JS pour la partie comportementale.
HTML pur et Web Components
C’est ici que la philosophie shadcn rencontre ses limites — et que des projets alternatifs prennent le relais.
Franken UI est le port le plus abouti pour le monde “HTML-first”. Construit sur UIkit 3, il reprend l’esthétique shadcn avec plus de 70 composants utilisables en HTML pur + Tailwind, sans React ni aucun framework JS. Le JavaScript nécessaire aux interactions (modals, dropdowns) vient d’UIkit, pas d’un framework de composants.
Source : Franken UI — Discussion #3140
CapsuleUI adopte une approche Web Components avec Lit. Le CLI fonctionne exactement comme celui de shadcn :
npx @zizigy/capsule init
npx @zizigy/capsule add Button
Le composant est copié localement dans un dossier @capsule, modifiable à volonté. Mais contrairement à shadcn, le résultat est un Web Component standard utilisable dans n’importe quel contexte — React, Vue, vanilla HTML, voire un CMS qui accepte du HTML custom. Le compromis : Shadow DOM pour l’isolation des styles, ce qui complique l’intégration avec Tailwind.
Source : CapsuleUI — DEV Community
Vanilla UI prend le chemin inverse : un port de Radix UI en Web Components Light DOM (pas de Shadow DOM), ce qui permet de styler les composants avec du CSS classique ou Tailwind sans barrière. C’est le projet qui se rapproche le plus d’un “shadcn sans framework” — mêmes primitives accessibles, même modèle de copie du code, mais en Web Components natifs.
Source : Vanilla UI — Introduction
Ce que ça révèle
Le modèle de distribution de shadcn (registre JSON + CLI + code source copié) est indépendant de React. Mais les primitives headless accessibles — le vrai travail dur — restent largement spécifiques à chaque framework. Le défi n’est pas de distribuer du code autrement, c’est de réécrire les primitives d’accessibilité pour chaque cible.
9. Pièges et limitations
Les dépendances npm ne disparaissent pas
shadcn/ui n’élimine pas les dépendances — il déplace le curseur. Radix UI, Tailwind CSS, class-variance-authority, clsx, lucide-react restent des paquets npm classiques dans votre package.json. Ce qui change, c’est que la couche au-dessus des primitives vous appartient.
Le coût de la propriété
Posséder le code signifie le maintenir. Sur un projet avec 30 composants shadcn, c’est 30 fichiers à surveiller pour les mises à jour de sécurité et d’accessibilité. La commande diff aide, mais elle ne remplace pas un dependabot.
Lock-in stylistique
Les composants shadcn sont conçus pour Tailwind CSS. Les classes sont écrites directement dans le JSX — il n’y a pas de couche d’abstraction entre le composant et le framework CSS. Si votre projet utilise CSS Modules ou Styled Components, deux options s’offrent à vous : réécrire les composants (ou utiliser un portage communautaire comme shadcn-css), ou utiliser directement Radix UI sans la couche shadcn. Comme vu dans la section sur le registre, l’outillage de distribution est agnostique — c’est le contenu qui est couplé.
Pas de versionning sémantique par composant
Quand le registre upstream met à jour un composant, il n’y a pas de numéro de version ni de changelog détaillé pour ce composant spécifique. Le diff montre ce qui a changé, mais pas pourquoi ni quel est le risque de ne pas mettre à jour.
10. Conclusion
shadcn/ui n’est pas une bibliothèque de composants au sens classique. C’est un outil de distribution de code source qui repose sur une conviction : le développeur devrait posséder le code de son interface.
En pratique :
- Le CLI automatise la copie des composants dans votre projet — le “copier-coller” est un raccourci de langage pour cette distribution automatisée
- Le registre (JSON + CLI) est le mécanisme qui rend cette distribution possible, y compris pour vos propres composants d’équipe
- La gestion des versions repose sur la commande
diffet une discipline de maintenance manuelle — un compromis assumé en échange du contrôle total
Le modèle fonctionne particulièrement bien pour les équipes qui construisent un design system sur mesure et veulent un point de départ solide sans les contraintes d’une dépendance npm opaque. Il fonctionne moins bien si vous cherchez une solution “batteries incluses” avec mises à jour automatiques.
Ce qui est remarquable, c’est que ce modèle de distribution — le registre JSON + CLI — est devenu un standard au-delà de shadcn lui-même. Des bibliothèques communautaires comme Magic UI ou des services comme Clerk publient désormais leurs composants dans des registres compatibles. L’idée a dépassé le projet.
Source : Introduction — shadcn/ui