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.
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.
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.
# 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.
{
"$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.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
// 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 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.
// 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> ) }
// 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> ) }
// 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> ) }
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.
npm install react-hook-form zod @hookform/resolvers npx shadcn@latest add form input select checkbox textarea
// 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> ) }
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.
npm install @tanstack/react-table npx shadcn@latest add table
// 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> ) }, }, ]
// 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> ) }
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.
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> ) }
/* 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%; } }
// 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> ) }
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.
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
// 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 }
// 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" />
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 →