Use Cases (Mis à jour: 02/06/2026)

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 avec Claude Code : guide React pratique

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().

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èglePourquoi
src/components/ui reste réservé aux primitives shadcn/uiLes différences sont traçables
src/components/app contient l’UI produitLa logique métier ne pollue pas la base
Ne pas modifier les alias components.json sans raisonLes imports restent stables
Donner à Claude Code les fichiers ciblesMoins 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.

#Claude Code #shadcn/ui #React #Tailwind CSS #Bibliothèque UI
Gratuit

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.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.