UI & Komponenten

shadcn/ui mit Claude Code: Modern React Component Library 2026

shadcn/ui revolutioniert wie React-UIs gebaut werden: Keine npm-Pakete, keine Black Box — du besitzt den Code. Claude Code versteht shadcn/ui vollständig und baut professionelle Interfaces in Minuten.

6. Mai 2026 10 min Lesezeit TypeScript & React
shadcn/ui Radix UI Tailwind CSS React Hook Form TanStack Table CVA

shadcn/ui ist 2026 der De-facto-Standard für React-Komponentenbibliotheken. Nicht weil es das größte npm-Paket ist — sondern weil es keins ist. Du kopierst den Komponentencode direkt in dein Projekt, besitzt ihn vollständig und kannst ihn nach Belieben anpassen. Claude Code versteht dieses Konzept in- und auswendig und generiert shadcn/ui-Komponenten, Custom Themes, Formular-Integrationen und vollständige Data Tables mit TanStack.

Inhalt

  1. shadcn/ui Setup & Initialisierung
  2. Core Components: Button, Card, Dialog & mehr
  3. Forms & Validation mit React Hook Form + Zod
  4. Data Tables mit TanStack Table
  5. Dark Mode & Custom Themes
  6. Eigene Komponenten mit Radix UI & CVA

1. shadcn/ui Setup & Initialisierung

Der erste Schritt mit shadcn/ui ist die Initialisierung via CLI. Claude Code übernimmt den gesamten Setup-Prozess — von der Tailwind-Konfiguration bis zur components.json und dem cn()-Hilfsklassensystem.

Projekt initialisieren

# Neues Next.js Projekt mit TypeScript + Tailwind
npx create-next-app@latest mein-projekt \
  --typescript \
  --tailwind \
  --eslint \
  --app \
  --src-dir

# shadcn/ui CLI initialisieren
cd mein-projekt
npx shadcn@latest init

Der shadcn init-Befehl fragt nach dem gewünschten Style (Default oder New York), der Base Color und dem CSS-Variables-Modus. Claude Code beantwortet diese Fragen automatisch basierend auf den Projekt-Anforderungen.

components.json Konfiguration

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "src/app/globals.css",
    "baseColor": "zinc",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  },
  "iconLibrary": "lucide"
}

Tailwind CSS Konfiguration

// tailwind.config.ts
import type { Config } from "tailwindcss"

const config: Config = {
  darkMode: ["class"],
  content: [
    "./pages/**/*.{ts,tsx}",
    "./components/**/*.{ts,tsx}",
    "./app/**/*.{ts,tsx}",
    "./src/**/*.{ts,tsx}",
  ],
  theme: {
    container: {
      center: true,
      padding: "2rem",
      screens: { "2xl": "1400px" },
    },
    extend: {
      colors: {
        border: "hsl(var(--border))",
        input: "hsl(var(--input))",
        ring: "hsl(var(--ring))",
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
        primary: {
          DEFAULT: "hsl(var(--primary))",
          foreground: "hsl(var(--primary-foreground))",
        },
        secondary: {
          DEFAULT: "hsl(var(--secondary))",
          foreground: "hsl(var(--secondary-foreground))",
        },
        destructive: {
          DEFAULT: "hsl(var(--destructive))",
          foreground: "hsl(var(--destructive-foreground))",
        },
        muted: {
          DEFAULT: "hsl(var(--muted))",
          foreground: "hsl(var(--muted-foreground))",
        },
        accent: {
          DEFAULT: "hsl(var(--accent))",
          foreground: "hsl(var(--accent-foreground))",
        },
      },
      borderRadius: {
        lg: "var(--radius)",
        md: "calc(var(--radius) - 2px)",
        sm: "calc(var(--radius) - 4px)",
      },
    },
  },
  plugins: [require("tailwindcss-animate")],
}

export default config

CN-Helper und Komponenten installieren

// src/lib/utils.ts — wird automatisch von shadcn init erstellt
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

---

# Einzelne Komponenten hinzufügen
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add dialog
npx shadcn@latest add input
npx shadcn@latest add label
npx shadcn@latest add form
npx shadcn@latest add table
npx shadcn@latest add badge
npx shadcn@latest add avatar
npx shadcn@latest add sheet
npx shadcn@latest add tooltip

# Oder mehrere auf einmal
npx shadcn@latest add button card dialog input label form badge avatar

shadcn/ui — Kernprinzipien


2. Core Components: Button, Card, Dialog & mehr

shadcn/ui bietet alle klassischen UI-Komponenten. Claude Code kennt jede Variante und setzt die richtigen Props, Klassen und Typen — ohne dass du die Dokumentation nachschlagen musst.

Button Varianten

// src/components/demo/ButtonDemo.tsx
import { Button } from "@/components/ui/button"
import { Loader2, Mail, Github } from "lucide-react"

export function ButtonDemo() {
  return (
    <div className="flex flex-wrap gap-3 items-center">
      {/* Standard Varianten */}
      <Button>Default</Button>
      <Button variant="destructive">Destructive</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="link">Link</Button>

      {/* Mit Icons */}
      <Button>
        <Mail className="mr-2 h-4 w-4" /> Email senden
      </Button>
      <Button variant="outline">
        <Github className="mr-2 h-4 w-4" /> GitHub
      </Button>

      {/* Loading State */}
      <Button disabled>
        <Loader2 className="mr-2 h-4 w-4 animate-spin" />
        Wird geladen...
      </Button>

      {/* Größen */}
      <Button size="sm">Klein</Button>
      <Button size="lg">Groß</Button>
      <Button size="icon"><Mail className="h-4 w-4" /></Button>
    </div>
  )
}

Card Komponente

// src/components/demo/CardDemo.tsx
import {
  Card, CardContent, CardDescription,
  CardFooter, CardHeader, CardTitle
} from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"

interface ProjectCardProps {
  title: string
  description: string
  status: "active" | "draft" | "archived"
  onEdit: () => void
  onDelete: () => void
}

export function ProjectCard({
  title, description, status, onEdit, onDelete
}: ProjectCardProps) {
  const statusVariant = {
    active: "default",
    draft: "secondary",
    archived: "outline",
  }[status] as "default" | "secondary" | "outline"

  return (
    <Card className="w-full max-w-sm">
      <CardHeader>
        <div className="flex items-center justify-between">
          <CardTitle className="text-lg">{title}</CardTitle>
          <Badge variant={statusVariant}>
            {status === "active" ? "Aktiv" : status === "draft" ? "Entwurf" : "Archiviert"}
          </Badge>
        </div>
        <CardDescription>{description}</CardDescription>
      </CardHeader>
      <CardContent>
        <p className="text-sm text-muted-foreground">
          Zuletzt bearbeitet: {new Date().toLocaleDateString("de-DE")}
        </p>
      </CardContent>
      <CardFooter className="flex gap-2">
        <Button onClick={onEdit} className="flex-1">Bearbeiten</Button>
        <Button variant="outline" onClick={onDelete}>Löschen</Button>
      </CardFooter>
    </Card>
  )
}

Dialog, Sheet und Tooltip

// src/components/demo/DialogDemo.tsx
import {
  Dialog, DialogContent, DialogDescription,
  DialogFooter, DialogHeader, DialogTitle, DialogTrigger
} from "@/components/ui/dialog"
import {
  Tooltip, TooltipContent, TooltipProvider, TooltipTrigger
} from "@/components/ui/tooltip"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export function ProjektDialog() {
  return (
    <TooltipProvider>
      <Dialog>
        <DialogTrigger asChild>
          <Tooltip>
            <TooltipTrigger asChild>
              <Button>Neues Projekt</Button>
            </TooltipTrigger>
            <TooltipContent>Erstelle ein neues Projekt</TooltipContent>
          </Tooltip>
        </DialogTrigger>

        <DialogContent className="sm:max-w-[425px]">
          <DialogHeader>
            <DialogTitle>Projekt erstellen</DialogTitle>
            <DialogDescription>
              Gib deinem Projekt einen Namen und eine kurze Beschreibung.
            </DialogDescription>
          </DialogHeader>

          <div className="grid gap-4 py-4">
            <div className="grid grid-cols-4 items-center gap-4">
              <Label htmlFor="name" className="text-right">Name</Label>
              <Input id="name" placeholder="Mein Projekt" className="col-span-3" />
            </div>
          </div>

          <DialogFooter>
            <Button type="submit">Erstellen</Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </TooltipProvider>
  )
}
Claude Code Tipp: Beschreibe einfach "Ich brauche einen Dialog zum Bearbeiten von Nutzerprofilen mit Avatar-Upload, Name und E-Mail" — Claude Code generiert den vollständigen Dialog mit Formular, Validierung und TypeScript-Typen automatisch.

3. Forms & Validation mit React Hook Form + Zod

shadcn/ui's Form-Komponenten sind speziell für die Verwendung mit React Hook Form und Zod ausgelegt. Claude Code kennt das Integrationsmuster exakt und generiert typensichere Formulare mit eingebetteter Fehlerbehandlung.

Abhängigkeiten installieren

npm install react-hook-form zod @hookform/resolvers
npx shadcn@latest add form input select checkbox textarea

Registrierungsformular mit Zod-Validierung

// src/components/forms/RegistrierungForm.tsx
"use client"

import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import {
  Form, FormControl, FormDescription,
  FormField, FormItem, FormLabel, FormMessage
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { toast } from "@/components/ui/use-toast"

// Zod Schema Definition
const registrierungSchema = z.object({
  email: z.string().email("Bitte gib eine gültige E-Mail-Adresse ein"),
  passwort: z.string()
    .min(8, "Mindestens 8 Zeichen")
    .regex(/[A-Z]/, "Mindestens einen Großbuchstaben")
    .regex(/[0-9]/, "Mindestens eine Zahl"),
  passwortBestaetigung: z.string(),
  vorname: z.string().min(2, "Mindestens 2 Zeichen"),
  nachname: z.string().min(2, "Mindestens 2 Zeichen"),
  agbAkzeptiert: z.boolean().refine(val => val === true, {
    message: "Du musst die AGB akzeptieren",
  }),
}).refine(data => data.passwort === data.passwortBestaetigung, {
  message: "Passwörter stimmen nicht überein",
  path: ["passwortBestaetigung"],
})

type RegistrierungFormValues = z.infer<typeof registrierungSchema>

export function RegistrierungForm() {
  const form = useForm<RegistrierungFormValues>({
    resolver: zodResolver(registrierungSchema),
    defaultValues: {
      email: "",
      passwort: "",
      passwortBestaetigung: "",
      vorname: "",
      nachname: "",
      agbAkzeptiert: false,
    },
  })

  async function onSubmit(values: RegistrierungFormValues) {
    try {
      // API-Call hier
      await fetch("/api/auth/register", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(values),
      })
      toast({ title: "Registrierung erfolgreich!", description: "Willkommen!" })
    } catch (error) {
      toast({ title: "Fehler", description: "Registrierung fehlgeschlagen.", variant: "destructive" })
    }
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 max-w-md">
        <div className="grid grid-cols-2 gap-4">
          <FormField
            control={form.control}
            name="vorname"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Vorname</FormLabel>
                <FormControl>
                  <Input placeholder="Max" {...field} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="nachname"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Nachname</FormLabel>
                <FormControl>
                  <Input placeholder="Mustermann" {...field} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
        </div>

        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>E-Mail</FormLabel>
              <FormControl>
                <Input type="email" placeholder="max@beispiel.de" {...field} />
              </FormControl>
              <FormDescription>Wir senden dir keine Spam-E-Mails.</FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="agbAkzeptiert"
          render={({ field }) => (
            <FormItem className="flex flex-row items-start space-x-3 space-y-0">
              <FormControl>
                <Checkbox
                  checked={field.value}
                  onCheckedChange={field.onChange}
                />
              </FormControl>
              <FormLabel>Ich akzeptiere die AGB und Datenschutzerklärung</FormLabel>
              <FormMessage />
            </FormItem>
          )}
        />

        <Button type="submit" className="w-full"
          disabled={form.formState.isSubmitting}>
          {form.formState.isSubmitting ? "Wird registriert..." : "Registrieren"}
        </Button>
      </form>
    </Form>
  )
}

Form-Integration Checkliste


4. Data Tables mit TanStack Table

shadcn/ui's Data Table ist eine Abstraktion über TanStack Table (früher React Table v8). Sie unterstützt Sorting, Filtering, Pagination und Row Selection out-of-the-box. Claude Code generiert vollständige Table-Setups inklusive Column-Definitionen.

Abhängigkeiten

npm install @tanstack/react-table
npx shadcn@latest add table

Column-Definitionen mit TypeScript

// src/components/tables/aufgaben-columns.tsx
"use client"

import { ColumnDef } from "@tanstack/react-table"
import { ArrowUpDown, MoreHorizontal } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Checkbox } from "@/components/ui/checkbox"
import {
  DropdownMenu, DropdownMenuContent, DropdownMenuItem,
  DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"

export type Aufgabe = {
  id: string
  titel: string
  status: "ausstehend" | "in-bearbeitung" | "abgeschlossen" | "abgebrochen"
  prioritaet: "niedrig" | "mittel" | "hoch" | "kritisch"
  faelligkeitsdatum: Date
  verantwortlicher: string
}

export const aufgabenColumns: ColumnDef<Aufgabe>[] = [
  {
    id: "select",
    header: ({ table }) => (
      <Checkbox
        checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
        onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
        aria-label="Alle auswählen"
      />
    ),
    cell: ({ row }) => (
      <Checkbox
        checked={row.getIsSelected()}
        onCheckedChange={(value) => row.toggleSelected(!!value)}
        aria-label="Zeile auswählen"
      />
    ),
    enableSorting: false,
    enableHiding: false,
  },
  {
    accessorKey: "titel",
    header: ({ column }) => (
      <Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
        Titel <ArrowUpDown className="ml-2 h-4 w-4" />
      </Button>
    ),
  },
  {
    accessorKey: "status",
    header: "Status",
    cell: ({ row }) => {
      const status = row.getValue<Aufgabe["status"]>("status")
      const variantMap: Record<Aufgabe["status"], string> = {
        "ausstehend": "secondary",
        "in-bearbeitung": "default",
        "abgeschlossen": "outline",
        "abgebrochen": "destructive",
      }
      return <Badge variant={variantMap[status] as any}>{status}</Badge>
    },
  },
  {
    accessorKey: "prioritaet",
    header: "Priorität",
    cell: ({ row }) => {
      const prio = row.getValue<string>("prioritaet")
      return <span className="capitalize font-medium">{prio}</span>
    },
  },
  {
    accessorKey: "faelligkeitsdatum",
    header: ({ column }) => (
      <Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
        Fälligkeitsdatum <ArrowUpDown className="ml-2 h-4 w-4" />
      </Button>
    ),
    cell: ({ row }) => {
      const date = row.getValue<Date>("faelligkeitsdatum")
      return date.toLocaleDateString("de-DE")
    },
  },
  {
    id: "actions",
    cell: ({ row }) => {
      const aufgabe = row.original
      return (
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <Button variant="ghost" className="h-8 w-8 p-0">
              <MoreHorizontal className="h-4 w-4" />
            </Button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end">
            <DropdownMenuLabel>Aktionen</DropdownMenuLabel>
            <DropdownMenuItem onClick={() => navigator.clipboard.writeText(aufgabe.id)}>
              ID kopieren
            </DropdownMenuItem>
            <DropdownMenuSeparator />
            <DropdownMenuItem>Bearbeiten</DropdownMenuItem>
            <DropdownMenuItem className="text-destructive">Löschen</DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      )
    },
  },
]

DataTable-Komponente mit Pagination und Filter

// src/components/tables/DataTable.tsx
"use client"

import { useState } from "react"
import {
  flexRender, getCoreRowModel, getFilteredRowModel,
  getPaginationRowModel, getSortedRowModel, useReactTable,
  SortingState, ColumnFiltersState, VisibilityState
} from "@tanstack/react-table"
import {
  Table, TableBody, TableCell, TableHead,
  TableHeader, TableRow
} from "@/components/ui/table"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"

interface DataTableProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[]
  data: TData[]
  filterColumn?: string
  filterPlaceholder?: string
}

export function DataTable<TData, TValue>({
  columns, data, filterColumn = "titel", filterPlaceholder = "Filtern..."
}: DataTableProps<TData, TValue>) {
  const [sorting, setSorting] = useState<SortingState>([])
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
  const [rowSelection, setRowSelection] = useState({})

  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    onRowSelectionChange: setRowSelection,
    state: { sorting, columnFilters, rowSelection },
    initialState: { pagination: { pageSize: 10 } },
  })

  return (
    <div className="space-y-4">
      <div className="flex items-center justify-between">
        <Input
          placeholder={filterPlaceholder}
          value={(table.getColumn(filterColumn)?.getFilterValue() as string) ?? ""}
          onChange={(e) => table.getColumn(filterColumn)?.setFilterValue(e.target.value)}
          className="max-w-sm"
        />
        <span className="text-sm text-muted-foreground">
          {table.getFilteredSelectedRowModel().rows.length} /{" "}
          {table.getFilteredRowModel().rows.length} ausgewählt
        </span>
      </div>

      <div className="rounded-md border">
        <Table>
          <TableHeader>
            {table.getHeaderGroups().map((headerGroup) => (
              <TableRow key={headerGroup.id}>
                {headerGroup.headers.map((header) => (
                  <TableHead key={header.id}>
                    {header.isPlaceholder ? null :
                      flexRender(header.column.columnDef.header, header.getContext())}
                  </TableHead>
                ))}
              </TableRow>
            ))}
          </TableHeader>
          <TableBody>
            {table.getRowModel().rows?.length ? (
              table.getRowModel().rows.map((row) => (
                <TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
                  {row.getVisibleCells().map((cell) => (
                    <TableCell key={cell.id}>
                      {flexRender(cell.column.columnDef.cell, cell.getContext())}
                    </TableCell>
                  ))}
                </TableRow>
              ))
            ) : (
              <TableRow>
                <TableCell colSpan={columns.length} className="h-24 text-center">
                  Keine Ergebnisse.
                </TableCell>
              </TableRow>
            )}
          </TableBody>
        </Table>
      </div>

      <div className="flex items-center justify-end gap-2">
        <Button variant="outline" size="sm"
          onClick={() => table.previousPage()}
          disabled={!table.getCanPreviousPage()}>Zurück</Button>
        <Button variant="outline" size="sm"
          onClick={() => table.nextPage()}
          disabled={!table.getCanNextPage()}>Weiter</Button>
      </div>
    </div>
  )
}

5. Dark Mode & Custom Themes

shadcn/ui verwendet CSS Variables für das komplette Farbsystem. Das macht es trivial, eigene Themes zu erstellen und Dark Mode zu implementieren. Claude Code generiert vollständige ThemeProvider-Setups mit next-themes.

next-themes Installation und Provider

npm install next-themes
// src/components/providers/ThemeProvider.tsx
"use client"

import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

---

// src/app/layout.tsx
import { ThemeProvider } from "@/components/providers/ThemeProvider"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="de" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

CSS Variables — Custom Theme

/* src/app/globals.css — Eigenes Indigo-Theme */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 224 71.4% 4.1%;
    --card: 0 0% 100%;
    --card-foreground: 224 71.4% 4.1%;
    --popover: 0 0% 100%;
    --popover-foreground: 224 71.4% 4.1%;
    --primary: 262.1 83.3% 57.8%;        /* Indigo */
    --primary-foreground: 210 20% 98%;
    --secondary: 220 14.3% 95.9%;
    --secondary-foreground: 220.9 39.3% 11%;
    --muted: 220 14.3% 95.9%;
    --muted-foreground: 220 8.9% 46.1%;
    --accent: 220 14.3% 95.9%;
    --accent-foreground: 220.9 39.3% 11%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 210 20% 98%;
    --border: 220 13% 91%;
    --input: 220 13% 91%;
    --ring: 262.1 83.3% 57.8%;
    --radius: 0.5rem;
  }

  .dark {
    --background: 224 71.4% 4.1%;
    --foreground: 210 20% 98%;
    --card: 224 71.4% 4.1%;
    --card-foreground: 210 20% 98%;
    --popover: 224 71.4% 4.1%;
    --popover-foreground: 210 20% 98%;
    --primary: 263.4 70% 50.4%;
    --primary-foreground: 210 20% 98%;
    --secondary: 215 27.9% 16.9%;
    --secondary-foreground: 210 20% 98%;
    --muted: 215 27.9% 16.9%;
    --muted-foreground: 217.9 10.6% 64.9%;
    --accent: 215 27.9% 16.9%;
    --accent-foreground: 210 20% 98%;
    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 210 20% 98%;
    --border: 215 27.9% 16.9%;
    --input: 215 27.9% 16.9%;
    --ring: 263.4 70% 50.4%;
  }
}

Theme-Switcher Komponente

// src/components/ThemeSwitcher.tsx
"use client"

import { useTheme } from "next-themes"
import { Moon, Sun, Monitor } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu, DropdownMenuContent,
  DropdownMenuItem, DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"

export function ThemeSwitcher() {
  const { setTheme, theme } = useTheme()

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon">
          <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
          <Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
          <span className="sr-only">Theme wechseln</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setTheme("light")}>
          <Sun className="mr-2 h-4 w-4" /> Hell
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("dark")}>
          <Moon className="mr-2 h-4 w-4" /> Dunkel
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("system")}>
          <Monitor className="mr-2 h-4 w-4" /> System
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Dark Mode Best Practices


6. Eigene Komponenten mit Radix UI & CVA

Das Mächtigste an shadcn/ui ist, dass du die gleichen Werkzeuge nutzen kannst, um eigene Komponenten im gleichen Stil zu bauen. Radix UI Primitives als barrierefreie Basis, CVA (Class Variance Authority) für Varianten und cn() für Klassen-Merging.

Abhängigkeiten für eigene Komponenten

npm install @radix-ui/react-slot class-variance-authority clsx tailwind-merge
npm install @radix-ui/react-progress @radix-ui/react-switch @radix-ui/react-slider

Eigene Status-Badge-Komponente mit CVA

// src/components/ui/status-indicator.tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const statusIndicatorVariants = cva(
  // Basis-Klassen (gelten immer)
  "inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors",
  {
    variants: {
      variant: {
        online:   "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
        offline:  "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400",
        warning:  "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
        error:    "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
        loading:  "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
      },
      size: {
        sm: "text-xs px-2 py-0.5",
        md: "text-sm px-3 py-1",
        lg: "text-base px-4 py-1.5",
      },
      pulse: {
        true: "animate-pulse",
        false: "",
      },
    },
    defaultVariants: {
      variant: "online",
      size: "md",
      pulse: false,
    },
  }
)

const dotVariants = cva("h-1.5 w-1.5 rounded-full", {
  variants: {
    variant: {
      online:  "bg-green-500",
      offline: "bg-gray-400",
      warning: "bg-yellow-500",
      error:   "bg-red-500",
      loading: "bg-blue-500 animate-spin",
    },
  },
  defaultVariants: { variant: "online" },
})

export interface StatusIndicatorProps
  extends React.HTMLAttributes<HTMLSpanElement>,
    VariantProps<typeof statusIndicatorVariants> {
  label?: string
}

const StatusIndicator = React.forwardRef<HTMLSpanElement, StatusIndicatorProps>(
  ({ className, variant, size, pulse, label, ...props }, ref) => {
    const labelMap: Record<string, string> = {
      online: "Online", offline: "Offline",
      warning: "Warnung", error: "Fehler", loading: "Laden...",
    }

    return (
      <span
        ref={ref}
        className={cn(statusIndicatorVariants({ variant, size, pulse }), className)}
        {...props}
      >
        <span className={cn(dotVariants({ variant }))} />
        {label ?? labelMap[variant ?? "online"]}
      </span>
    )
  }
)
StatusIndicator.displayName = "StatusIndicator"

export { StatusIndicator, statusIndicatorVariants }

Radix UI Progress Bar — shadcn-Stil

// src/components/ui/fortschrittsbalken.tsx
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"

interface FortschrittsbalkenProps
  extends React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> {
  label?: string
  showValue?: boolean
  color?: "default" | "success" | "warning" | "destructive"
}

const Fortschrittsbalken = React.forwardRef<
  React.ElementRef<typeof ProgressPrimitive.Root>,
  FortschrittsbalkenProps
>(({ className, value, label, showValue = false, color = "default", ...props }, ref) => {
  const colorClasses = {
    default:     "bg-primary",
    success:     "bg-green-500",
    warning:     "bg-yellow-500",
    destructive: "bg-destructive",
  }

  return (
    <div className="space-y-1.5">
      {(label || showValue) && (
        <div className="flex justify-between text-sm">
          {label && <span className="text-foreground font-medium">{label}</span>}
          {showValue && <span className="text-muted-foreground">{value ?? 0}%</span>}
        </div>
      )}
      <ProgressPrimitive.Root
        ref={ref}
        className={cn("relative h-2 w-full overflow-hidden rounded-full bg-secondary", className)}
        {...props}
      >
        <ProgressPrimitive.Indicator
          className={cn("h-full transition-all duration-500 ease-in-out", colorClasses[color])}
          style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}
        />
      </ProgressPrimitive.Root>
    </div>
  )
})
Fortschrittsbalken.displayName = ProgressPrimitive.Root.displayName

export { Fortschrittsbalken }

---

// Verwendung
<Fortschrittsbalken value={67} label="Upload-Fortschritt" showValue color="success" />
<Fortschrittsbalken value={23} label="Speicher" showValue color="warning" />
<Fortschrittsbalken value={92} label="CPU-Auslastung" showValue color="destructive" />
Claude Code + shadcn/ui Workflow: Beschreibe deine gewünschte Komponente in natürlicher Sprache — "Ich brauche eine Preisvergleichstabelle mit Toggle zwischen monatlich und jährlich, 3 Preisklassen, hervorgehobener mittlerer Plan und CTA-Buttons" — Claude Code generiert die vollständige TypeScript-Komponente im shadcn/ui-Stil, inklusive Animationen, Dark Mode und barrierefreier Semantik.

Eigene Komponenten — Checkliste


shadcn/ui + Claude Code = produktionsreife UIs in Minuten

shadcn/ui Components, TanStack Tables, Zod-validierte Formulare, Dark Mode Themes und eigene Radix-Komponenten — Claude Code versteht das gesamte Ökosystem und generiert TypeScript-Code, der direkt in deine Projekte passt. Jetzt kostenlos testen und das erste professionelle React-Interface bauen.

14 Tage kostenlos testen →