Use Cases (Aktualisiert: 2.6.2026)

shadcn/ui mit Claude Code: Praxisguide für React

Claude Code mit shadcn/ui nutzen: Setup, Button/Card/Form/Dialog/Table, Design Tokens und Playwright-Checks.

shadcn/ui mit Claude Code: Praxisguide für React

shadcn/ui ist keine klassische Komponentenbibliothek, die unsichtbar in node_modules bleibt. Die CLI kopiert den Quellcode der Komponenten in dein Projekt, meist nach src/components/ui. Dadurch besitzt dein Team Button, Card, Dialog und Table wirklich selbst. Das ist praktisch, aber es macht diese Dateien auch zu wartbarem Produktcode.

Genau deshalb passt shadcn/ui gut zu Claude Code. Claude Code kann die generierten Dateien lesen, Tailwind-Klassen nachvollziehen, Varianten anpassen, Formulare verdrahten und Playwright-Tests ergänzen. Ohne klare Grenzen kann Claude Code aber auch fast gleiche Buttons erzeugen, alte Tailwind-Konfiguration einfügen oder wichtige Dialog-Struktur für Barrierefreiheit entfernen.

Dieser Guide zeigt einen sicheren Ablauf für Vite + React + TypeScript: shadcn/ui installieren, Button/Card/Form/Dialog/Table hinzufügen, Design Tokens ordnen, Copy-Paste-Drift vermeiden und das Ergebnis mit Playwright visuell prüfen. Wenn du Claude Code noch nicht sicher nutzt, beginne mit dem Claude Code Einstiegsguide.

Mit offiziellen Quellen starten

Gib Claude Code vor der Bearbeitung die aktuellen Primärquellen. Die shadcn/ui-Dokumentation für Vite nutzt shadcn@latest, Tailwind CSS v4 beschreibt Theme-Variablen über @theme, Radix UI dokumentiert das zugängliche Dialog-Verhalten, und Playwright nutzt toHaveScreenshot() für visuelle Vergleiche.

flowchart LR
  A["Claude Code prüft das Projekt"] --> B["shadcn/ui initialisieren"]
  B --> C["Basiskomponenten hinzufügen"]
  C --> D["Design Tokens zentralisieren"]
  D --> E["App-Komponenten bauen"]
  E --> F["Mit Playwright absichern"]

Installation und Initialisierung

Das Beispiel nutzt Vite, weil die Oberfläche klein bleibt. Mit Next.js funktioniert das Prinzip ebenfalls, aber für Einsteiger ist es besser, Routing und Server Components nicht gleichzeitig mit dem UI-System zu lernen.

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

Konfiguriere Tailwind und das @-Alias in 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"),
    },
  },
})

Dasselbe Alias gehört in die TypeScript-Konfiguration.

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Bitte Claude Code zuerst um eine Prüfung statt um sofortige Änderungen.

Ich möchte shadcn/ui in dieses Vite + React + TypeScript Projekt einbauen.
Lies zuerst package.json, vite.config.ts, tsconfig*.json und src/index.css.
Melde nur fehlende Einstellungen. Nach meiner Freigabe führe shadcn@latest init aus
und füge die benötigten Komponenten hinzu.

Danach laufen die Befehle:

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

Die aktuellen shadcn/ui-Formularbeispiele setzen auf React Hook Form, Zod und Field. Alte Blog-Snippets mit anderer Struktur sollten nicht ungeprüft übernommen werden.

Button und Card zuerst

Starte nicht mit einem kompletten Dashboard. Baue zuerst eine kleine Komponente, die Button und Card nutzt und sich leicht reviewen lässt. shadcn/ui-generierte Dateien bleiben in src/components/ui; produktspezifische UI kommt nach 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>
  )
}

Ein enger Review-Prompt reduziert Nebenänderungen:

Reviewe nur src/components/app/ProjectSummaryCard.tsx.
Prüfe Props-Typen, shadcn/ui-Imports, Lesbarkeit der Tailwind-Klassen und Barrierefreiheit.
Schlage Änderungen an anderen Dateien nur vor, wenn sie nötig sind, und begründe sie.

Form mit Field, React Hook Form und Zod

Ein Formular besteht nicht nur aus hübschen Inputs. Produktreif bedeutet: Validierung, Fehlermeldungen, aria-invalid, deaktivierter Submit während der Verarbeitung, Autocomplete und später Serverfehler.

"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>
  )
}

Bei kleinen Formularen dürfen Schema und Komponente zusammenbleiben. Wenn Felder wachsen, sollte Claude Code bewusst in schema.ts, ContactForm.tsx und Server Action trennen.

Dialog und Table sicher kombinieren

Dialog ist kein beliebiger Overlay-Container. Radix UI kümmert sich um Modalmodus, Fokusfalle, Escape-Schließen und Screenreader-Ankündigungen über Title und Description. shadcn/ui liefert Styling, aber die Struktur muss erhalten bleiben.

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>
    </>
  )
}

In echten Apps sollte die Seite Daten laden und customers sowie onEdit übergeben. So bleibt die UI-Komponente klein und Claude Code ändert nicht aus Versehen die Datenlogik.

Design Tokens und Tailwind-Konfiguration

Design Tokens sind benannte Werte für Farbe, Radius, Abstände, Schatten und Schrift. Tailwind v4 erzeugt Utilities über @theme; shadcn/ui nutzt außerdem CSS-Variablen wie --background, --primary und --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);
}

Wenn ein Projekt noch Tailwind v3 nutzt, lass Claude Code zwei getrennte Pläne schreiben: Migration auf v4 oder minimaler v3-kompatibler Patch. Beides in einem Diff zu mischen erzeugt schwer nachvollziehbare Fehler.

Copy-Paste-Drift vermeiden

shadcn/ui gibt dir Quellcode. Ohne Regeln entstehen schnell mehrere fast gleiche Buttons oder Cards.

RegelNutzen
src/components/ui bleibt shadcn/ui-Primitiven vorbehaltenDiffs bleiben nachvollziehbar
Produktspezifische UI liegt in src/components/appGeschäftslogik bleibt aus der Basis heraus
components.json-Alias nicht beiläufig ändernImports bleiben stabil
Claude Code konkrete Zieldateien gebenWeniger Duplikate
Die Dateien in src/components/ui stammen aus shadcn/ui.
Erzeuge keinen neuen Button oder Card.
Nutze bestehende Imports und lege produktspezifische UI in src/components/app ab.
Führe danach rg "function .*Button|export .*Button" src/components aus
und melde, ob Duplikate entstanden sind.
git diff -- src/components/ui
git diff -- src/components/app
pnpm lint
pnpm build

Weitere Arbeitsregeln findest du in den Claude Code Produktivitätstipps.

Visuelle Prüfung mit Playwright

Playwright erzeugt eine Referenzaufnahme und vergleicht spätere Läufe damit. Betriebssystem, Browser, Fonts und Viewport sollten möglichst dem CI-Setup entsprechen.

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,
  })
})

Snapshots werden nur aktualisiert, wenn die visuelle Änderung beabsichtigt ist.

pnpm playwright test --update-snapshots

Bei Fehlern hilft ein präziser Prompt.

Der Playwright-Check customers-dialog.png ist fehlgeschlagen.
Auf Mobile sind die Buttons im Dialog-Footer zu eng.
Lies nur src/components/app/CustomerTable.tsx und zugehöriges CSS.
Korrigiere den Abstand, aber entferne DialogTitle und DialogDescription nicht.

Drei sinnvolle Einsatzfälle

Erstens: ein neues Admin-Interface. Button, Card, Table und Dialog decken Liste, Detail, Bearbeitung und Bestätigung ab. Baue zuerst statische UI und verbinde danach die API.

Zweitens: ein bestehendes Produkt vereinheitlichen. Ersetze zuerst kleine Bereiche wie Suchformular, Einstellungsdialog oder Empty-State-Card, statt alles auf einmal neu zu bauen.

Drittens: bezahlte Prototypen oder Kundendemos. Mock-Daten sind okay, aber Komponenten-Grenzen sollten produktionsnah bleiben, damit die Demo nicht weggeworfen werden muss.

Auch Barrierefreiheit passt gut in diesen Ablauf. Nutze ergänzend den Claude Code Accessibility Guide, um Labels, Tastaturbedienung, Dialog-Struktur und Fehlermeldungen zu prüfen.

Häufige Stolperfallen

Die häufigste Falle ist alte Tailwind-Konfiguration in einem v4-Projekt. Prüfe zuerst Version und Build-Pipeline.

Die zweite Falle ist Geschäftslogik in components/ui. Eine Button-Variante für Zahlungspläne gehört in einen App-Wrapper, nicht in die Basis-Komponente.

Die dritte Falle ist das Entfernen von DialogTitle, weil der Titel visuell stört. Wenn er nicht sichtbar sein soll, verstecke ihn bewusst, statt die Information zu löschen.

Die vierte Falle ist Vertrauen auf required allein. Produktformulare brauchen übersetzte Fehler, asynchrone Validierung und Serverfehler.

Die fünfte Falle ist eine instabile Playwright-Umgebung. Fonts und Viewport können bereits Unterschiede erzeugen.

Prompt-Vorlage und getestetes Ergebnis

Ziel: shadcn/ui in einen Vite + React + TypeScript Admin-Screen einbauen.
Button, Card, Field-basiertes Form, Dialog und Table verwenden.

Vorgaben:
- Aktuelle offizielle Dokumentation beachten
- src/components/ui nur für shadcn/ui-Primitiven nutzen
- App-Komponenten in src/components/app ablegen
- Formulare mit React Hook Form + Zod + Field bauen
- DialogTitle und DialogDescription behalten
- Tailwind v4 @theme und CSS-Variablen erklären
- Einen Playwright toHaveScreenshot-Test hinzufügen

Ablauf:
1. Bestehende Konfiguration prüfen
2. Änderungsplan vorschlagen
3. Nach Freigabe implementieren
4. pnpm build und pnpm playwright test ausführen
5. Geänderte Dateien und Risiken zusammenfassen

Getestetes Ergebnis: In einer minimalen Vite-App ließen sich Komponenteninstallation, Markenfarbe über @theme, Field-basiertes Formular, Dialog aus einer Table-Zeile und ein Playwright-Screenshot-Check reproduzieren. Eine zweite Runde war nur beim Fehler-Rendering im Formular nötig; mit explizitem aria-invalid und FieldError wurde der Diff klar.

Für Teams gehört diese Vorlage in CLAUDE.md. ClaudeCodeLab bietet solche UI-Regeln, Review-Checklisten und Claude-Code-Prompts auch als kostenpflichtige Templates an, damit wiederkehrende Sprint-Erklärungen nicht jedes Mal neu geschrieben werden müssen.

#Claude Code #shadcn/ui #React #Tailwind CSS #UI-Bibliothek
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.