shadcn/ui avec Claude Code : guide React pratique
Utilisez Claude Code avec shadcn/ui : installation, Button/Card/Form/Dialog/Table, tokens et contrôles Playwright.
shadcn/ui n’est pas une bibliothèque de composants classique que l’on installe puis que l’on oublie dans node_modules. Sa CLI copie le code des composants dans votre projet, souvent dans src/components/ui. Vous possédez donc le code de Button, Card, Dialog ou Table, avec toute la liberté et toute la responsabilité que cela implique.
C’est précisément pour cela que shadcn/ui fonctionne bien avec Claude Code. Claude Code peut lire les composants générés, comprendre vos classes Tailwind, modifier une variante, brancher un formulaire et ajouter un test Playwright. Mais une demande trop vague comme “crée un dashboard” peut produire des boutons dupliqués, une configuration Tailwind obsolète ou un Dialog moins accessible.
Ce guide présente un flux fiable pour une application Vite + React + TypeScript : installer shadcn/ui, ajouter Button/Card/Form/Dialog/Table, organiser les design tokens, éviter la dérive par copier-coller et vérifier l’interface avec Playwright. Pour les bases de Claude Code, commencez par le guide de démarrage Claude Code.
Partir des sources officielles
Avant de modifier le projet, donnez à Claude Code les références actuelles. La documentation Vite de shadcn/ui utilise shadcn@latest, Tailwind CSS v4 décrit les variables de thème via @theme, Radix UI explique le comportement accessible de Dialog, et Playwright documente les comparaisons visuelles avec toHaveScreenshot().
- shadcn/ui Vite installation
- shadcn/ui React Hook Form guide
- Tailwind CSS theme variables
- Radix UI Dialog docs
- Claude Code quickstart
- Playwright visual comparisons
flowchart LR
A["Claude Code inspecte le projet"] --> B["Initialiser shadcn/ui"]
B --> C["Ajouter les composants"]
C --> D["Centraliser les tokens"]
D --> E["Créer la couche app"]
E --> F["Vérifier avec Playwright"]
Installation et initialisation
L’exemple utilise Vite pour rester simple. Les mêmes principes s’appliquent à Next.js, mais il vaut mieux ne pas mélanger dès le début système UI, routing et Server Components.
pnpm create vite@latest shadcn-claude-demo -- --template react-ts
cd shadcn-claude-demo
pnpm install
pnpm add tailwindcss @tailwindcss/vite
pnpm add -D @types/node
Configurez Tailwind et l’alias @ dans vite.config.ts.
import path from "node:path"
import react from "@vitejs/plugin-react"
import tailwindcss from "@tailwindcss/vite"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})
Ajoutez le même alias côté TypeScript.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
La bonne demande à Claude Code commence par une inspection.
Je veux ajouter shadcn/ui à ce projet Vite + React + TypeScript.
Lis d'abord package.json, vite.config.ts, tsconfig*.json et src/index.css.
Signale uniquement la configuration manquante. Après validation,
exécute shadcn@latest init et ajoute les composants nécessaires.
Puis lancez les commandes.
pnpm dlx shadcn@latest init
pnpm dlx shadcn@latest add button card field input label dialog table
pnpm add react-hook-form zod @hookform/resolvers
La documentation actuelle de shadcn/ui pour les formulaires s’appuie sur React Hook Form, Zod et le composant Field. Ne copiez pas un ancien exemple sans vérifier sa compatibilité.
Button et Card : commencer petit
Le premier composant doit être limité et facile à relire. Gardez les composants générés par shadcn/ui dans src/components/ui et placez les composants métier dans src/components/app.
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
type ProjectSummaryCardProps = {
name: string
openIssues: number
onCreateTask: () => void
}
export function ProjectSummaryCard({
name,
openIssues,
onCreateTask,
}: ProjectSummaryCardProps) {
return (
<Card className="max-w-md">
<CardHeader>
<CardTitle>{name}</CardTitle>
<CardDescription>
Review open UI issues before starting the next task.
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-3xl font-semibold">{openIssues}</p>
<p className="text-muted-foreground text-sm">open issues</p>
</CardContent>
<CardFooter>
<Button onClick={onCreateTask}>Add task</Button>
</CardFooter>
</Card>
)
}
Demandez une revue ciblée.
Relis uniquement src/components/app/ProjectSummaryCard.tsx.
Vérifie les types de props, les imports shadcn/ui, la lisibilité Tailwind et l'accessibilité.
Ne propose d'autres fichiers que si c'est indispensable, avec la raison.
Un vrai Form avec Field, React Hook Form et Zod
Un formulaire n’est pas seulement une liste d’inputs. En production, il faut valider, afficher les erreurs, définir aria-invalid, désactiver l’envoi pendant le traitement et prévoir les erreurs serveur.
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
const contactSchema = z.object({
email: z.string().email("Enter a valid email address"),
topic: z.string().min(4, "Use at least 4 characters"),
})
type ContactFormValues = z.infer<typeof contactSchema>
export function ContactForm() {
const form = useForm<ContactFormValues>({
resolver: zodResolver(contactSchema),
defaultValues: {
email: "",
topic: "",
},
})
function onSubmit(values: ContactFormValues) {
console.log("submit", values)
}
return (
<form className="max-w-md space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
name="email"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
<Input
{...field}
id={field.name}
type="email"
aria-invalid={fieldState.invalid}
autoComplete="email"
/>
<FieldDescription>We will use this address to reply.</FieldDescription>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
</FieldGroup>
<Button type="submit" disabled={form.formState.isSubmitting}>
Send
</Button>
</form>
)
}
Pour un petit formulaire, le schéma et le composant peuvent rester ensemble. Quand le formulaire grandit, demandez une séparation explicite entre schema.ts, ContactForm.tsx et l’action serveur.
Dialog et Table sans casser l’accessibilité
Dialog n’est pas une simple fenêtre flottante. Radix UI gère le mode modal, le piège de focus, la fermeture avec Escape et les annonces lecteur d’écran via Title et Description. shadcn/ui fournit le style, pas une excuse pour supprimer cette structure.
import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
type Customer = {
id: string
name: string
plan: "Free" | "Pro" | "Team"
}
const customers: Customer[] = [
{ id: "cus_001", name: "Aoi Tanaka", plan: "Pro" },
{ id: "cus_002", name: "Mika Sato", plan: "Team" },
]
export function CustomerTable() {
const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>(null)
return (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>Customer</TableHead>
<TableHead>Plan</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{customers.map((customer) => (
<TableRow key={customer.id}>
<TableCell className="font-medium">{customer.name}</TableCell>
<TableCell>{customer.plan}</TableCell>
<TableCell className="text-right">
<Button
variant="outline"
size="sm"
onClick={() => setSelectedCustomer(customer)}
>
Edit
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Dialog open={selectedCustomer !== null} onOpenChange={() => setSelectedCustomer(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit customer</DialogTitle>
<DialogDescription>
Review the contract plan for {selectedCustomer?.name}.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setSelectedCustomer(null)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}
Dans une application réelle, gardez le fetch dans la page et passez customers plus onEdit au composant. Claude Code pourra modifier l’UI sans toucher à la couche de données.
Tokens de design et configuration Tailwind
Les design tokens sont des valeurs nommées pour les couleurs, rayons, espacements, ombres et polices. Dans Tailwind v4, @theme influence les utilitaires générés. Les composants shadcn/ui utilisent aussi des variables CSS comme --background, --primary et --border.
@import "tailwindcss";
@theme {
--font-sans: Inter, system-ui, sans-serif;
--radius-card: 0.75rem;
--color-brand-500: oklch(0.62 0.18 250);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
}
Si le projet utilise encore tailwind.config.ts, demandez deux plans séparés : migration vers v4 ou correctif minimal compatible v3. Mélanger les deux rend le debug pénible.
Éviter la dérive par copier-coller
shadcn/ui vous donne le code source. C’est puissant, mais sans règles vous obtiendrez vite plusieurs Button presque identiques.
| Règle | Pourquoi |
|---|---|
src/components/ui reste réservé aux primitives shadcn/ui | Les différences sont traçables |
src/components/app contient l’UI produit | La logique métier ne pollue pas la base |
Ne pas modifier les alias components.json sans raison | Les imports restent stables |
| Donner à Claude Code les fichiers cibles | Moins de doublons |
Les fichiers src/components/ui viennent de shadcn/ui.
Ne crée pas de nouveau Button ou Card.
Utilise les imports existants et place l'UI produit dans src/components/app.
Après modification, exécute rg "function .*Button|export .*Button" src/components
et indique si des doublons sont apparus.
git diff -- src/components/ui
git diff -- src/components/app
pnpm lint
pnpm build
Pour renforcer votre méthode de travail, voyez aussi les conseils de productivité Claude Code.
Vérification visuelle avec Playwright
Playwright crée une capture de référence puis compare les exécutions suivantes. L’OS, le navigateur, les polices et le viewport doivent être aussi proches que possible de la CI.
pnpm create playwright
pnpm playwright install
pnpm dev
pnpm playwright test
import { expect, test } from "@playwright/test"
test("customer table dialog visual state", async ({ page }) => {
await page.goto("/customers")
await page.getByRole("button", { name: "Edit" }).first().click()
await expect(page).toHaveScreenshot("customers-dialog.png", {
maxDiffPixels: 120,
})
})
Mettez à jour les snapshots uniquement pour un changement visuel voulu.
pnpm playwright test --update-snapshots
Quand un test échoue, donnez à Claude Code un problème précis.
La capture customers-dialog.png échoue.
Sur mobile, les boutons du footer du Dialog sont trop proches.
Lis seulement src/components/app/CustomerTable.tsx et le CSS lié.
Corrige l'espacement sans supprimer DialogTitle ni DialogDescription.
Trois cas d’usage
Premier cas : construire un nouvel admin. Button, Card, Table et Dialog couvrent liste, détail, édition et confirmation. Demandez d’abord une UI avec données statiques, puis branchez l’API.
Deuxième cas : harmoniser un produit existant. Remplacez un formulaire de recherche, un Dialog de réglages ou une Card d’état vide avant d’envisager une refonte globale.
Troisième cas : réaliser des prototypes payants ou des démos client. Les données mock sont acceptables, mais les frontières de composants doivent rester proches de la production.
L’accessibilité est un autre bon terrain. Associez ce flux au guide accessibilité avec Claude Code pour vérifier labels, clavier, structure Dialog et erreurs de formulaire.
Pièges fréquents
Le premier piège est de coller une configuration Tailwind ancienne dans un projet v4. Vérifiez la version et le pipeline avant de modifier.
Le deuxième est de placer de la logique métier dans components/ui. Une variante de Button liée à un plan payant doit vivre dans une couche app, pas dans la primitive.
Le troisième est de supprimer DialogTitle pour gagner de la place. Si le titre ne doit pas être visible, cachez-le volontairement tout en conservant l’information.
Le quatrième est de compter uniquement sur required. Les formulaires sérieux ont besoin d’erreurs traduites, de validation asynchrone et d’erreurs serveur.
Le cinquième est de générer les captures Playwright dans un environnement différent de la CI. Les polices et le viewport comptent.
Prompt prêt à l’emploi et résultat
Objectif : ajouter shadcn/ui à un écran admin Vite + React + TypeScript.
Utiliser Button, Card, Form basé sur Field, Dialog et Table.
Contraintes :
- Suivre les docs officielles actuelles
- Garder src/components/ui pour les primitives shadcn/ui
- Mettre les composants app dans src/components/app
- Utiliser React Hook Form + Zod + Field
- Conserver DialogTitle et DialogDescription
- Expliquer les changements Tailwind v4 @theme et variables CSS
- Ajouter un test Playwright toHaveScreenshot
Processus :
1. Inspecter la configuration
2. Proposer un plan
3. Implémenter après validation
4. Lancer pnpm build et pnpm playwright test
5. Résumer les fichiers changés et les risques
Résultat testé : dans une application Vite minimale, ce flux a permis d’installer les composants, d’ajouter une couleur de marque avec @theme, de créer un formulaire avec Field, d’ouvrir un Dialog depuis une ligne de Table et de lancer une comparaison visuelle Playwright. La seule seconde passe a concerné l’affichage des erreurs de formulaire ; demander explicitement aria-invalid et FieldError a rendu le diff beaucoup plus net.
Pour une équipe, placez ce prompt dans CLAUDE.md. ClaudeCodeLab propose aussi des templates payants avec règles UI, checklists de revue et prompts Claude Code pour éviter de répéter les mêmes consignes à chaque sprint.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.