Was ist shadcn/ui?
Wer shadcn/ui zum ersten Mal begegnet, ist verwirrt: Es gibt keine einzige NPM-Bibliothek namens shadcn-ui zu installieren. Stattdessen kopiert das CLI die Komponenten-Dateien direkt in dein Projekt — vollständig anpassbar, vollständig unter deiner Kontrolle.
KonzeptWas shadcn/ui wirklich ist
# shadcn/ui IST NICHT:
# Ein NPM-Paket das du importierst
# Eine fertige Komponentenbibliothek (wie MUI oder Chakra UI)
# Etwas das du updaten musst (keine Breaking Changes durch Library-Updates)
# shadcn/ui IST:
# Eine Sammlung von Copy-Paste-faehigen Komponenten
# Code in deinem Projekt (src/components/ui/)
# Vollstaendig anpassbar (du besitzt den Code)
# Basiert auf Radix UI Primitives (Accessibility eingebaut)
# Styled mit Tailwind CSS und CSS Variables
# Die Philosophie (aus der shadcn/ui Doku):
# "Diese Komponenten sind dein Ausgangspunkt, nicht dein Endpunkt."
# "Kopiere den Code. Mach ihn dir zu eigen. Passe ihn an deine Beduerfnisse an."
# Was das CLI tatsaechlich macht:
npx shadcn@latest add button
# Legt an: src/components/ui/button.tsx
# Der Code ist direkt in deinem Projekt
# Keine Abhaengigkeit zu einem "shadcn" Package
# Nur Abhaengigkeiten: @radix-ui/react-slot + class-variance-authority
| Eigenschaft | shadcn/ui | MUI / Chakra UI |
| Code-Ownership | Du besitzt den Code | Library-Package |
| Anpassbarkeit | 100% (Code direkt editieren) | Beschränkt auf Theme-API |
| Bundle-Größe | Nur was du nutzt | Gesamte Library |
| Breaking Changes | Keine (dein Code bleibt) | Mit jedem Major Update |
| Styling-System | Tailwind + CSS Variables | Eigenes Theme-System |
| Accessibility | Radix UI Primitives | Teilweise |
| Dark Mode | Automatisch via CSS Variables | Manuell konfigurieren |
Claude Code Prompt: "Erkläre mir den Unterschied zwischen shadcn/ui und einer klassischen Komponentenbibliothek wie MUI. Was bedeutet Copy-Paste-First für unsere Codebasis?" — Claude erklärt die Philosophie und zeigt direkt passende Anwendungsfälle für dein Projekt.
Installation und Setup
shadcn/ui benötigt ein bestehendes Next.js- oder Vite-Projekt mit Tailwind CSS. Das Init-Kommando richtet alles ein — von der components.json bis zu den CSS Variables im globalen Stylesheet.
Setupshadcn/ui von Null in 5 Minuten
# 1. Next.js Projekt erstellen (oder bestehendes nutzen)
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
# 2. shadcn/ui initialisieren
npx shadcn@latest init
# CLI fragt:
# Which style would you like to use? Default
# Which color would you like to use as base color? Slate
# Would you like to use CSS variables for colors? Yes
# Ergebnis: components.json (Projekt-Konfiguration)
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
# Was init ausserdem macht:
# Fuegt CSS Variables in app/globals.css ein
# Erstellt lib/utils.ts mit cn()-Hilfsfunktion
# Konfiguriert tailwind.config.ts fuer shadcn-Farben
# Installiert: tailwindcss-animate, class-variance-authority, clsx, tailwind-merge
UtilsDie cn()-Funktion — das Herzstück
# lib/utils.ts automatisch generiert
import { clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import type { ClassValue } from "clsx"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
# clsx fasst konditionelle Klassen zusammen
# twMerge loest Tailwind-Konflikte auf (p-2 + p-4 wird nur p-4)
# Beispiel:
cn(
"px-4 py-2 rounded-md",
isActive && "bg-primary text-primary-foreground",
isDisabled && "opacity-50 cursor-not-allowed",
className // Props-Klassen ueberschreiben interne
)
# Ergebnis: "px-4 py-2 rounded-md bg-primary text-primary-foreground"
Claude Code Prompt: "Richte shadcn/ui in meinem bestehenden Vite + React Projekt ein. Tailwind ist bereits konfiguriert." — Claude Code erkennt das bestehende Setup und passt den Init-Prozess entsprechend an, ohne Tailwind-Konfiguration zu überschreiben.
Komponenten hinzufügen
Jede Komponente wird einzeln zum Projekt hinzugefügt. Das CLI analysiert Abhängigkeiten und installiert automatisch alle benötigten Radix-Primitives.
KomponentenWas wird generiert — am Beispiel Button und Dialog
# Einzelne Komponenten hinzufuegen:
npx shadcn@latest add button
npx shadcn@latest add dialog
npx shadcn@latest add table
npx shadcn@latest add form
# Mehrere auf einmal:
npx shadcn@latest add button card badge input select
# Was bei "add button" passiert:
# Installiert: @radix-ui/react-slot
# Erstellt: src/components/ui/button.tsx
# Inhalt von src/components/ui/button.tsx:
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
# Nutzung im JSX:
<Button variant="outline" size="sm">Abbrechen</Button>
<Button variant="destructive">Loeschen</Button>
<Button variant="ghost" size="icon"><TrashIcon /></Button>
DialogDialog-Komponente — Radix Composition Pattern
# Was "add dialog" installiert:
# @radix-ui/react-dialog
# src/components/ui/dialog.tsx
# Nutzung im Code:
import {
Dialog, DialogContent, DialogDescription,
DialogFooter, DialogHeader, DialogTitle, DialogTrigger
} from "@/components/ui/dialog"
export function DeleteDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">Konto loeschen</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Bist du sicher?</DialogTitle>
<DialogDescription>
Diese Aktion kann nicht rueckgaengig gemacht werden.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">Abbrechen</Button>
<Button variant="destructive">Endgueltig loeschen</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
# asChild-Pattern: DialogTrigger rendert KEINEN eigenen Button
# Nutzt stattdessen das Kind-Element als Trigger
# Kein doppeltes Nesting von button in button
Claude Code Prompt: "Füge shadcn/ui Table, Form und DataTable zu unserem Projekt hinzu. Erstelle eine Beispiel-Tabelle mit Pagination für Benutzerdaten." — Claude Code fügt die Komponenten hinzu und generiert direkt eine vollständige DataTable-Implementierung mit TanStack Table.
Themes und CSS Variables
Das Theme-System von shadcn/ui basiert vollständig auf CSS Custom Properties. Dark Mode ist keine Nachbearbeitung — er ist von Anfang an eingebaut, weil alle Farben als semantische Variablen definiert sind.
ThemeCSS Variables — das komplette System
/* app/globals.css von shadcn/ui init generiert */
@layer base {
:root {
/* Hintergrundfarben */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
/* Karten */
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
/* Popovers und Dropdowns */
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
/* Primaerfarbe fuer Buttons, Links, aktive Elemente */
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
/* Sekundaerfarbe */
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
/* Gedaempfte Elemente und Platzhalter */
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
/* Hover-Effekte */
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
/* Fehler und gefahrliche Aktionen */
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
/* Eingabefelder */
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--destructive: 0 62.8% 30.6%;
}
}
Dark Modenext-themes Integration — automatisch
# Installation:
npm install next-themes
# app/providers.tsx:
"use client"
import { ThemeProvider } from "next-themes"
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
)
}
# app/layout.tsx:
import { Providers } from "./providers"
export default function RootLayout({ children }) {
return (
<html lang="de" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
# Theme-Toggle Komponente:
import { useTheme } from "next-themes"
import { Sun, Moon } from "lucide-react"
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<Button variant="ghost" size="icon"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
<Sun className="dark:hidden" />
<Moon className="hidden dark:block" />
</Button>
)
}
# Warum das funktioniert:
# next-themes setzt class="dark" auf das html-Element
# .dark { --background: ... } ueberschreibt alle CSS Variables
# ALLE shadcn-Komponenten nutzen diese Variables - Dark Mode automatisch
Wichtig: Die CSS Variables nutzen HSL-Werte ohne den hsl()-Wrapper — z.B. 222.2 84% 4.9% statt hsl(222.2, 84%, 4.9%). Das ist Absicht: Tailwind ergänzt das hsl() automatisch, sodass du Opacity-Modifier wie bg-primary/50 nutzen kannst.
Claude Code Prompt: "Erstelle ein benutzerdefiniertes shadcn/ui Theme in Brandfarben: Primär #6366f1, Sekundär #8b5cf6. Generiere alle nötigen CSS Variables für Light und Dark Mode." — Claude berechnet die HSL-Werte und erstellt die komplette globals.css-Anpassung.
Form-Komponenten mit React Hook Form + Zod
shadcn/ui liefert eine vollständige Form-Integration: Form, FormField, FormControl, FormLabel, FormMessage — alles zusammen mit React Hook Form und Zod-Validation.
FormKomplettes Login-Formular mit Validation
# Pakete installieren:
npm install react-hook-form zod @hookform/resolvers
npx shadcn@latest add form input button
# components/login-form.tsx:
"use client"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import {
Form, FormControl, FormField, FormItem,
FormLabel, FormMessage
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
// Zod-Schema definiert Felder und Validierungsregeln:
const loginSchema = z.object({
email: z.string()
.min(1, "E-Mail ist erforderlich")
.email("Ungueltige E-Mail-Adresse"),
password: z.string()
.min(8, "Passwort muss mindestens 8 Zeichen haben")
.max(100, "Passwort zu lang"),
})
type LoginValues = z.infer<typeof loginSchema>
export function LoginForm() {
const form = useForm<LoginValues>({
resolver: zodResolver(loginSchema),
defaultValues: { email: "", password: "" },
})
async function onSubmit(values: LoginValues) {
// values ist vollstaendig typisiert und validiert
await loginUser(values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>E-Mail</FormLabel>
<FormControl>
<Input placeholder="name@firma.de" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Passwort</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full"
disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? "Anmelden..." : "Anmelden"}
</Button>
</form>
</Form>
)
}
SelectSelect, Checkbox und komplexe Feldtypen
# Weitere Felder mit shadcn/ui Form-Komponenten:
npx shadcn@latest add select checkbox switch textarea
# Zod-Schema fuer komplexes Formular:
const profileSchema = z.object({
name: z.string().min(2, "Name zu kurz"),
role: z.enum(["admin", "editor", "viewer"], {
required_error: "Bitte Rolle auswaehlen",
}),
notifications: z.boolean().default(false),
bio: z.string().max(500, "Maximal 500 Zeichen").optional(),
})
# FormField fuer Select:
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Rolle</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Rolle auswaehlen" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="admin">Administrator</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="viewer">Betrachter</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
# Cross-Field Validation mit z.refine():
const registerSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwoerter stimmen nicht ueberein",
path: ["confirmPassword"],
})
Claude Code Prompt: "Erstelle ein Registrierungsformular mit E-Mail, Passwort, Passwort-Bestätigung und AGBs-Checkbox. Nutze shadcn/ui Form-Komponenten mit Zod-Validation — Passwörter müssen übereinstimmen." — Claude Code kennt das z.refine()-Pattern für Cross-Field-Validation und implementiert es vollständig.
Eigene Komponenten erstellen
Das Mächtigste an shadcn/ui ist das Composition-Muster: Eigene Komponenten folgen denselben Konventionen wie die eingebauten — CVA für Variants, cn() für Klassen, Radix Primitives wo sinnvoll.
CVAClass Variance Authority — Variants wie die Profis
# CVA ist das Tool hinter shadcn/ui Button-Variants
# Eigene Komponente mit denselben Patterns:
# components/ui/status-badge.tsx:
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const statusBadgeVariants = cva(
"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-semibold",
{
variants: {
status: {
active: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
inactive: "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300",
pending: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300",
error: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
},
size: {
sm: "px-2 py-px text-[10px]",
md: "px-2.5 py-0.5 text-xs",
lg: "px-3 py-1 text-sm",
},
},
defaultVariants: {
status: "inactive",
size: "md",
},
}
)
interface StatusBadgeProps
extends React.HTMLAttributes<HTMLSpanElement>,
VariantProps<typeof statusBadgeVariants> {
showDot?: boolean
}
export function StatusBadge({
className, status, size, showDot = true, ...props
}: StatusBadgeProps) {
return (
<span className={cn(statusBadgeVariants({ status, size }), className)} {...props}>
{showDot && <span className="h-1.5 w-1.5 rounded-full bg-current" />}
{props.children}
</span>
)
}
# Nutzung:
<StatusBadge status="active">Aktiv</StatusBadge>
<StatusBadge status="error" size="lg">Fehler</StatusBadge>
<StatusBadge status="pending" showDot={false}>Ausstehend</StatusBadge>
CompositionCompound Components — Daten-Karte als Beispiel
# Eigene Compound-Komponente im shadcn/ui-Stil:
# components/ui/stat-card.tsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { cn } from "@/lib/utils"
interface StatCardProps {
title: string
value: string | number
description?: string
icon?: React.ReactNode
trend?: { value: number; isPositive: boolean }
className?: string
}
export function StatCard({
title, value, description, icon, trend, className
}: StatCardProps) {
return (
<Card className={cn("overflow-hidden", className)}>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{title}
</CardTitle>
{icon && <div className="text-muted-foreground">{icon}</div>}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
{description && (
<p className="text-xs text-muted-foreground mt-1">{description}</p>
)}
{trend && (
<div className={cn(
"flex items-center gap-1 text-xs mt-2 font-medium",
trend.isPositive ? "text-green-600" : "text-red-600"
)}>
{trend.isPositive ? "aufwaerts" : "abwaerts"} {Math.abs(trend.value)}%
<span className="text-muted-foreground font-normal">zum Vormonat</span>
</div>
)}
</CardContent>
</Card>
)
}
# Nutzung im Dashboard:
<div className="grid gap-4 md:grid-cols-3">
<StatCard
title="Monatlicher Umsatz"
value="4.280 EUR"
description="Gesamt im April 2026"
trend={{ value: 12, isPositive: true }}
/>
<StatCard
title="Aktive Nutzer"
value="1.234"
trend={{ value: 3, isPositive: false }}
/>
</div>
Registryshadcn/ui Registry — eigene Komponenten teilen
# Seit shadcn/ui 2.0: eigene Registry fuer Team-Komponenten
# registry.json in deinem Projekt:
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "meine-firma",
"homepage": "https://components.meine-firma.de",
"items": [
{
"name": "stat-card",
"type": "registry:component",
"description": "KPI-Karte mit Trend-Anzeige",
"dependencies": ["lucide-react"],
"registryDependencies": ["card"],
"files": [
{
"path": "registry/stat-card.tsx",
"type": "registry:component"
}
]
}
]
}
# Komponente aus eigener Registry hinzufuegen:
npx shadcn@latest add https://components.meine-firma.de/r/stat-card.json
# Team-Mitglieder koennen sofort alle internen Komponenten nutzen
# Gleicher Workflow wie offizielle shadcn/ui Komponenten
# Versionierung ueber Git oder Package-Registry
Claude Code Prompt: "Erstelle eine vollständige DataTable-Komponente mit shadcn/ui Table, Sorting, Filtering und Pagination. Nutze TanStack Table als Basis und folge dem shadcn/ui Composition-Pattern." — Claude Code erstellt eine vollständige, typsichere DataTable die dem shadcn/ui-Stil exakt entspricht.
Fazit: shadcn/ui mit Claude Code
shadcn/ui ist der klügste Ansatz im UI-Ökosystem 2026: Kein Lock-in, volle Kontrolle, erstklassige Accessibility durch Radix, und ein Theme-System das Dark Mode kostenlos mitliefert. Claude Code kennt jeden Aspekt — von der CLI-Konfiguration über CVA-Patterns bis zu komplexen Form-Setups mit Zod.
Der entscheidende Vorteil gegenüber klassischen Komponentenbibliotheken: Wenn eine Komponente nicht exakt deinen Anforderungen entspricht, editierst du sie einfach. Kein API-Overhead, kein Warten auf Library-Updates, keine Konflikte mit deinem Design-System. Und Claude Code weiß genau, welche Stellen es zu ändern gilt.
UI-Modul im Kurs
Im Claude Code Mastery Kurs: vollständiges UI-Modul mit shadcn/ui Setup, Theme-Konfiguration, Form-Patterns mit React Hook Form + Zod, eigene Komponenten-Registry und DataTable-Implementierung — inkl. Dark Mode und Accessibility-Audit.
14 Tage kostenlos testen →