Was ist Radix UI? Headless vs. Opinionated
Die meisten UI-Bibliotheken kommen mit vorgefertigten Styles — praktisch, aber einschränkend. Radix UI geht einen anderen Weg: Headless Primitives die alle Accessibility-Anforderungen erfüllen, ohne eine einzige CSS-Regel vorzuschreiben.
HeadlessRadix vs. opinionated Libraries im Vergleich
| Eigenschaft | Radix UI | MUI / Chakra UI | shadcn/ui |
| Eigene Styles | ✗ Keine | ✓ Material / Custom | ✓ Tailwind (kopierbar) |
| WAI-ARIA komplett | ✓ Built-in | ✓ Meist | ✓ Via Radix |
| Keyboard Navigation | ✓ Komplett | ✓ Meist | ✓ Via Radix |
| Bundle Size (Tree-Shaking) | ✓ Per-Paket | ✗ Groß | ✓ Nur was du nutzt |
| Styling-Freiheit | ✓ Vollständig | ✗ Eingeschränkt | ✓ Vollständig |
| Focus Trapping | ✓ Automatisch | ✓ Meist | ✓ Via Radix |
shadcn/ui Tipp: shadcn/ui ist kein Package — es ist eine Sammlung von Radix-basierten Komponenten die du direkt in dein Projekt kopierst. Claude Code kann shadcn-Komponenten generieren, anpassen und erweitern — vollständig Radix-basiert.
WAI-ARIAWas Radix automatisch implementiert
# Prompt: "Erkläre was Radix UI automatisch für Accessibility bereitstellt"
// Was du schreibst:
<Dialog.Root>
<Dialog.Trigger>Öffnen</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title>Mein Dialog</Dialog.Title>
<Dialog.Description>Beschreibung</Dialog.Description>
</Dialog.Content>
</Dialog.Root>
// Was Radix im DOM erzeugt:
<button
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="dialog-content-id"
>Öffnen</button>
<div
role="dialog"
id="dialog-content-id"
aria-modal="true"
aria-labelledby="dialog-title-id"
aria-describedby="dialog-desc-id"
tabindex="-1"
>
<h2 id="dialog-title-id">Mein Dialog</h2>
<p id="dialog-desc-id">Beschreibung</p>
</div>
// Automatisch dabei:
// → Focus wird auf Dialog-Content gesetzt beim Öffnen
// → Focus-Trap: Tab bleibt im Dialog
// → Escape schließt den Dialog
// → Scroll-Lock auf Body
// → Focus kehrt zum Trigger zurück beim Schließen
// → Screen Reader-Ankündigung via aria-live
InstallationRadix Pakete und Projektsetup
# Prompt: "Installiere Radix UI Primitives für mein React + Tailwind Projekt"
# Einzelne Pakete — nur was du brauchst:
npm install @radix-ui/react-dialog
npm install @radix-ui/react-dropdown-menu
npm install @radix-ui/react-tabs
npm install @radix-ui/react-accordion
npm install @radix-ui/react-toast
npm install @radix-ui/react-popover
npm install @radix-ui/react-select
npm install @radix-ui/react-checkbox
npm install @radix-ui/react-switch
npm install @radix-ui/react-tooltip
# Oder alle auf einmal (für größere Projekte):
npm install @radix-ui/react-{dialog,dropdown-menu,tabs,accordion,toast,popover,select,checkbox,switch,tooltip,label,separator,slot}
# Hilfspakete für Animationen:
npm install class-variance-authority clsx tailwind-merge
npm install tailwindcss-animate # Für Tailwind-Animationen
Tree-Shaking: Jeder Radix Primitive ist ein eigenes npm-Paket. Du zahlst im Bundle nur für das, was du tatsächlich importierst. Dialog allein sind ~8 KB gzipped — deutlich weniger als eine komplette UI-Bibliothek.
Dialog und AlertDialog: Modals korrekt umgesetzt
Der Dialog ist der komplexeste UI-Primitive überhaupt — Focus Trapping, ARIA-Rollen, Escape-Key, Portal-Rendering, Scroll-Lock. Radix löst all das. Claude Code weiß genau, wann Dialog vs. AlertDialog angemessen ist.
PortalControlled Dialog mit Tailwind-Styling
// components/ui/Dialog.tsx
// Prompt: "Erstelle einen vollständig gestylten Radix Dialog mit Tailwind"
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { cn } from '@/lib/utils'
// Overlay: Hintergrund-Dimmer mit Fade-Animation
const DialogOverlay = React.forwardRef<...>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/60 backdrop-blur-sm",
"data-[state=open]:animate-in data-[state=open]:fade-in-0",
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
className
)}
{...props}
/>
))
// Content: Der eigentliche Dialog mit Slide-Animation
const DialogContent = React.forwardRef<...>(({ className, children, ...props }, ref) => (
<DialogPrimitive.Portal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 w-full max-w-lg",
"translate-x-[-50%] translate-y-[-50%]",
"bg-white rounded-xl shadow-2xl p-6",
"data-[state=open]:animate-in data-[state=open]:fade-in-0",
"data-[state=open]:zoom-in-95 data-[state=open]:slide-in-from-top-4",
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
"data-[state=closed]:zoom-out-95",
className
)}
{...props}
>
{children}
// Schließen-Button — Radix setzt bereits Escape-Key-Handling!
<DialogPrimitive.Close
className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100
focus:outline-none focus:ring-2 focus:ring-violet-500"
aria-label="Schließen"
>
<X className="h-4 w-4" />
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
))
// Nutzung — controlled state:
const [open, setOpen] = useState(false)
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild>
<button className="bg-violet-600 text-white px-4 py-2 rounded-lg">
Dialog öffnen
</button>
</Dialog.Trigger>
<DialogContent>
<Dialog.Title className="text-lg font-semibold mb-2">
Einstellungen
</Dialog.Title>
<Dialog.Description className="text-sm text-gray-500 mb-4">
Passe dein Profil und deine Präferenzen an.
</Dialog.Description>
</* ... Form-Content ... */>
</DialogContent>
</Dialog.Root>
asChild Pattern: asChild übergibt alle Props (inklusive ARIA) an das Kind-Element statt ein eigenes DOM-Element zu rendern. So bleibt dein HTML-Markup sauber und du hast volle Kontrolle über den gerenderten Tag.
AlertDialogDestruktive Aktionen korrekt absichern
// AlertDialog: Für irreversible Aktionen (Löschen, Abbrechen etc.)
// Prompt: "AlertDialog für Bestätigung einer Lösch-Aktion"
import * as AlertDialog from '@radix-ui/react-alert-dialog'
function DeleteConfirmDialog({ onConfirm }: { onConfirm: () => void }) {
return (
<AlertDialog.Root>
<AlertDialog.Trigger asChild>
<button className="text-red-600 hover:text-red-700">
Projekt löschen
</button>
</AlertDialog.Trigger>
<AlertDialog.Portal>
<AlertDialog.Overlay className="fixed inset-0 bg-black/50" />
<AlertDialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2
-translate-y-1/2 bg-white rounded-xl p-6 max-w-md shadow-2xl">
<AlertDialog.Title className="text-lg font-semibold text-red-600">
Projekt unwiderruflich löschen?
</AlertDialog.Title>
<AlertDialog.Description className="text-sm text-gray-600 mt-2 mb-6">
Diese Aktion kann nicht rückgängig gemacht werden. Alle Daten,
Dateien und Konfigurationen werden permanent gelöscht.
</AlertDialog.Description>
<div className="flex gap-3 justify-end">
<// Cancel: Kein autofocus, Nutzer muss aktiv bestätigen>
<AlertDialog.Cancel className="px-4 py-2 border rounded-lg hover:bg-gray-50">
Abbrechen
</AlertDialog.Cancel>
<AlertDialog.Action
onClick={onConfirm}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
Ja, endgültig löschen
</AlertDialog.Action>
</div>
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog.Root>
)
}
// Unterschied Dialog vs AlertDialog:
// Dialog: Escape schließt, Klick auf Overlay schließt — für nicht-destruktive Aktionen
// AlertDialog: KEIN Escape-Close, KEIN Overlay-Close — Nutzer MUSS wählen
Wichtiger Unterschied: AlertDialog verhindert das Schließen per Escape oder Overlay-Klick. Das ist kein Bug — WAI-ARIA schreibt vor, dass destruktive Bestätigungen eine explizite Entscheidung erfordern. Claude Code setzt das automatisch korrekt um.
DropdownMenu und ContextMenu: Keyboard-Navigation inklusive
Menüs sind überraschend komplex: Sub-Menus, Checkboxen, Radio-Gruppen, Keyboard-Navigation mit Pfeiltasten und Buchstabensuche — Radix implementiert den gesamten ARIA Authoring Practices Guide.
KeyboardDropdownMenu mit Sub-Menus und Checkboxen
// Prompt: "Dropdown mit Sub-Menu, Checkboxen und Radio-Gruppe in Tailwind"
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
const menuItemClass = cn(
"flex items-center gap-2 px-3 py-1.5 text-sm rounded-md cursor-default",
"select-none outline-none",
"focus:bg-violet-50 focus:text-violet-900",
"data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed"
)
function ActionsMenu() {
const [showGrid, setShowGrid] = useState(true)
const [theme, setTheme] = useState("system")
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button className="flex items-center gap-1 px-3 py-2 rounded-lg
hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-violet-500">
Aktionen <ChevronDown className="h-4 w-4" />
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
className="z-50 min-w-48 bg-white rounded-xl shadow-lg p-1 border border-gray-200
data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95
data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2"
sideOffset={5}
align="end"
>
<// Normale Items>
<DropdownMenu.Item className={menuItemClass} onSelect={() => handleEdit()}>
<Pencil className="h-4 w-4" /> Bearbeiten
</DropdownMenu.Item>
<DropdownMenu.Separator className="h-px bg-gray-200 my-1" />
<// Checkbox-Item>
<DropdownMenu.CheckboxItem
className={menuItemClass}
checked={showGrid}
onCheckedChange={setShowGrid}
>
<DropdownMenu.ItemIndicator>
<Check className="h-4 w-4 text-violet-600" />
</DropdownMenu.ItemIndicator>
Gitter anzeigen
</DropdownMenu.CheckboxItem>
<DropdownMenu.Separator className="h-px bg-gray-200 my-1" />
<// Sub-Menu>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger className={cn(menuItemClass, "justify-between")}>
<span>Farbschema</span>
<ChevronRight className="h-4 w-4" />
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent className="..." sideOffset={2}>
<DropdownMenu.RadioGroup value={theme} onValueChange={setTheme}>
{["light", "dark", "system"].map(t => (
<DropdownMenu.RadioItem key={t} value={t} className={menuItemClass}>
<DropdownMenu.ItemIndicator>
<Circle className="h-2 w-2 fill-violet-600" />
</DropdownMenu.ItemIndicator>
{t === "light" ? "Hell" : t === "dark" ? "Dunkel" : "System"}
</DropdownMenu.RadioItem>
))}
</DropdownMenu.RadioGroup>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
)
}
Keyboard-Navigation automatisch: Pfeiltasten navigieren zwischen Items, Buchstaben springen zum ersten passenden Item, Enter/Space aktivieren, Escape schließt, Tab verlässt das Menü. Alles ohne eine Zeile eigenen Event-Handler-Code.
ContextMenuRechtsklick-Menü für Desktop-UIs
// Prompt: "Rechtsklick-Menü für File-Explorer mit Radix ContextMenu"
import * as ContextMenu from '@radix-ui/react-context-menu'
function FileItem({ file }: { file: File }) {
return (
<ContextMenu.Root>
<ContextMenu.Trigger asChild>
<div
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-50 cursor-pointer"
// Long-Press auf Touch-Geräten = Context-Menü
>
<FileIcon /> {file.name}
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content
className="min-w-48 bg-white rounded-xl shadow-xl p-1 border border-gray-200
animate-in fade-in-0 zoom-in-95"
>
<ContextMenu.Item className={menuItemClass}>
<Eye className="h-4 w-4" /> Vorschau
</ContextMenu.Item>
<ContextMenu.Item className={menuItemClass}>
<Download className="h-4 w-4" /> Herunterladen
</ContextMenu.Item>
<ContextMenu.Item className={menuItemClass}>
<Share className="h-4 w-4" /> Teilen...
</ContextMenu.Item>
<ContextMenu.Separator className="h-px bg-gray-200 my-1" />
<ContextMenu.Item
className={cn(menuItemClass, "text-red-600 focus:bg-red-50 focus:text-red-700")}
>
<Trash className="h-4 w-4" /> Löschen
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu.Root>
)
}
// API ist identisch mit DropdownMenu!
// → Selbe Item-Typen: Item, CheckboxItem, RadioItem, Sub
// → Trigger unterschied: DropdownMenu = Klick, ContextMenu = Rechtsklick
// → Touch: Long-Press löst ContextMenu.Trigger aus
Tabs und Accordion: Animiert mit Tailwind
Tabs und Accordion — zwei der meistgenutzten UI-Patterns. Radix implementiert beide nach dem ARIA Tabs Pattern und ARIA Accordion Pattern, inklusive korrekter Keyboard-Navigation und dynamischem Content-Aufbau.
AnimateTabs mit CSS Animationen und data-state
// Prompt: "Radix Tabs mit animiertem Tab-Indicator und Content-Transition"
import * as Tabs from '@radix-ui/react-tabs'
const tabs = [
{ value: "overview", label: "Übersicht" },
{ value: "analytics", label: "Analytik" },
{ value: "settings", label: "Einstellungen" },
]
function DashboardTabs() {
return (
<Tabs.Root defaultValue="overview" className="w-full">
<Tabs.List
className="flex gap-1 border-b border-gray-200 mb-6"
aria-label="Dashboard Abschnitte"
>
{tabs.map(tab => (
<Tabs.Trigger
key={tab.value}
value={tab.value}
className="px-4 py-2 text-sm font-medium rounded-t-lg -mb-px
text-gray-500 hover:text-gray-700
data-[state=active]:text-violet-700
data-[state=active]:border-b-2
data-[state=active]:border-violet-700
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500
transition-colors duration-150"
>
{tab.label}
</Tabs.Trigger>
))}
</Tabs.List>
// Tab-Content mit Fade-In-Animation
<Tabs.Content
value="overview"
className="data-[state=active]:animate-in data-[state=active]:fade-in-0
data-[state=active]:slide-in-from-bottom-2 duration-200 outline-none"
>
<OverviewPanel />
</Tabs.Content>
<Tabs.Content value="analytics" className="...">
<AnalyticsPanel />
</Tabs.Content>
<Tabs.Content value="settings" className="...">
<SettingsPanel />
</Tabs.Content>
</Tabs.Root>
)
}
// Keyboard-Navigation automatisch:
// → ArrowLeft/Right: zwischen Tabs navigieren
// → Home: erster Tab
// → End: letzter Tab
// → Automatic activation OR Manual (activationMode="manual")
activationMode="manual": Standardmäßig aktiviert der Fokus den Tab sofort (automatic). Mit activationMode="manual" navigierst du per Pfeiltaste und aktivierst mit Enter/Space. Für Tabs mit lazy-geladenem Content empfohlen.
AccordionAnimated Accordion mit CSS Height-Transition
// Prompt: "FAQ-Accordion mit smooth height animation via Tailwind"
import * as Accordion from '@radix-ui/react-accordion'
const faqItems = [
{
value: "item-1",
question: "Was kostet Claude Code im Vergleich zu Copilot?",
answer: "Claude Code berechnet per Token auf API-Basis..."
},
// weitere Items...
]
function FAQAccordion() {
return (
<Accordion.Root
type="single" // "single" = nur eines offen | "multiple" = mehrere
collapsible // Erlaubt Schließen des aktuell offenen Items
className="w-full divide-y divide-gray-200 border border-gray-200 rounded-xl overflow-hidden"
>
{faqItems.map(item => (
<Accordion.Item key={item.value} value={item.value}>
<Accordion.Header>
<Accordion.Trigger
className="flex w-full items-center justify-between p-4 text-left
font-medium text-gray-900 hover:bg-gray-50
data-[state=open]:bg-violet-50 data-[state=open]:text-violet-900
focus-visible:outline-none focus-visible:ring-2
focus-visible:ring-inset focus-visible:ring-violet-500
transition-colors group"
>
{item.question}
<ChevronDown
className="h-4 w-4 text-gray-400 transition-transform duration-200
group-data-[state=open]:rotate-180"
/>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content
className="overflow-hidden text-sm text-gray-600
data-[state=open]:animate-accordion-down
data-[state=closed]:animate-accordion-up"
>
<div className="p-4 pt-0">{item.answer}</div>
</Accordion.Content>
</Accordion.Item>
))}
</Accordion.Root>
)
}
// tailwind.config.js — Accordion Height Animation:
module.exports = {
theme: { extend: { keyframes: {
"accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" } },
"accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" } },
}, animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
}}}
}
CSS Custom Properties: Radix setzt --radix-accordion-content-height und --radix-accordion-content-width dynamisch. Du kannst diese in Tailwind-Animationen nutzen um smooth height transitions ohne JavaScript zu bauen.
Barrierefreie UIs mit KI-Unterstützung bauen
Claude Code kennt alle Radix Primitives und generiert vollständig barrierefreie Komponenten nach WAI-ARIA Standard — inklusive Keyboard-Navigation, Focus Management und Screen Reader Support.
14 Tage kostenlos testen →
Toast / Sonner: Benachrichtigungen mit Swipe-to-Dismiss
Toasts sind temporäre Benachrichtigungen die im Hintergrund erscheinen ohne den Nutzer zu unterbrechen. Radix Toast implementiert das korrekte ARIA Live Region Pattern — Screen Reader werden korrekt informiert ohne den Fokus zu unterbrechen.
ARIA LiveRadix Toast: Vollständige Implementation
// Prompt: "Toast-System mit Provider, Hook und animiertem Viewport"
import * as Toast from '@radix-ui/react-toast'
import { useState, useCallback } from 'react'
// 1. Provider + Viewport im Root-Layout
function App() {
return (
<Toast.Provider swipeDirection="right" duration={4000}>
<MyApp />
// Viewport: wo Toasts erscheinen (aria-live="polite" automatisch)
<Toast.Viewport
className="fixed bottom-4 right-4 flex flex-col gap-2 z-[100]
max-w-[420px] w-full m-0 list-none outline-none"
/>
</Toast.Provider>
)
}
// 2. Custom Hook für imperatives API
function useToast() {
const [toasts, setToasts] = useState<ToastItem[]>([])
const toast = useCallback(({ title, description, variant = "default" }) => {
setToasts(prev => [...prev, { id: Date.now(), title, description, variant, open: true }])
}, [])
return { toast, toasts, setToasts }
}
// 3. Toast-Komponente mit Swipe-Animation
function ToastItem({ title, description, variant, open, onOpenChange }) {
return (
<Toast.Root
open={open}
onOpenChange={onOpenChange}
className={cn(
"bg-white rounded-xl shadow-lg border p-4 flex items-start gap-3",
"data-[state=open]:animate-in data-[state=open]:fade-in-0",
"data-[state=open]:slide-in-from-bottom-4",
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
"data-[state=closed]:slide-out-to-right-full",
"data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)]",
"data-[swipe=end]:animate-out data-[swipe=end]:slide-out-to-right-full",
variant === "success" && "border-green-200 bg-green-50",
variant === "error" && "border-red-200 bg-red-50",
)}
>
<div className="flex-1 min-w-0">
<Toast.Title className="font-semibold text-sm text-gray-900">
{title}
</Toast.Title>
{description && (
<Toast.Description className="text-xs text-gray-500 mt-0.5">
{description}
</Toast.Description>
)}
</div>
<Toast.Close
className="text-gray-400 hover:text-gray-600 rounded focus:outline-none
focus:ring-2 focus:ring-violet-500"
aria-label="Toast schließen"
>
<X className="h-4 w-4" />
</Toast.Close>
</Toast.Root>
)
}
// 4. Nutzung im Code:
const { toast } = useToast()
async function handleSave() {
try {
await saveData()
toast({ title: "Gespeichert!", variant: "success", description: "Änderungen wurden übernommen." })
} catch {
toast({ title: "Fehler", variant: "error", description: "Speichern fehlgeschlagen." })
}
}
Sonner als Alternative: sonner ist eine kompakte Toast-Bibliothek (Emil Kowalski) die intern Radix-Patterns nutzt. Für einfachere Use-Cases reicht import { toast } from 'sonner'. Für volle Kontrolle und ARIA-Compliance ist Radix Toast empfohlen.
Styling-Patterns: Tailwind CSS + Radix Data Attributes
Das Herzstück des Radix-Stylings sind data-state-Attribute. Jeder Primitive setzt diese automatisch — du reagierst mit Tailwind-Klassen oder CSS-Selektoren darauf.
data-stateAlle data-* Attribute im Überblick
// Radix setzt data-Attribute automatisch — du stylst darauf:
// data-state (fast alle Primitives)
"data-[state=open]:opacity-100" // Sichtbar wenn geöffnet
"data-[state=closed]:opacity-0" // Unsichtbar wenn geschlossen
"data-[state=active]:border-violet-600" // Aktiver Tab/Accordion
"data-[state=checked]:bg-violet-600" // Checkbox/Switch checked
"data-[state=indeterminate]:bg-gray-400" // Checkbox indeterminate
"data-[state=on]:bg-violet-100" // Toggle gedrückt
// data-side (Popover, Tooltip, DropdownMenu)
"data-[side=top]:slide-in-from-bottom-2" // Erscheint oben
"data-[side=bottom]:slide-in-from-top-2" // Erscheint unten
"data-[side=left]:slide-in-from-right-2" // Erscheint links
"data-[side=right]:slide-in-from-left-2" // Erscheint rechts
// data-disabled, data-highlighted (Menu Items)
"data-[disabled]:opacity-50"
"data-[disabled]:cursor-not-allowed"
"data-[highlighted]:bg-violet-50"
// data-swipe (Toast)
"data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)]"
"data-[swipe=end]:slide-out-to-right-full"
"data-[swipe=cancel]:translate-x-0 data-[swipe=cancel]:transition-transform"
// CSS Custom Properties (Radix setzt diese per JS):
"--radix-accordion-content-height" // → für height-Animationen
"--radix-accordion-content-width" // → für width-Animationen
"--radix-tooltip-content-transform-origin" // → für scale-Animationen
"--radix-toast-swipe-move-x" // → live Swipe-Position
CVAclass-variance-authority für Varianten-Management
// Prompt: "Button-Komponente mit CVA und Radix Slot"
import { cva, type VariantProps } from 'class-variance-authority'
import { Slot } from '@radix-ui/react-slot'
const buttonVariants = cva(
// Base classes (immer aktiv):
"inline-flex items-center justify-center rounded-lg font-medium transition-all
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500
disabled:opacity-50 disabled:cursor-not-allowed select-none",
{
variants: {
variant: {
default: "bg-violet-600 text-white hover:bg-violet-700 shadow-sm",
outline: "border border-gray-300 bg-white hover:bg-gray-50 text-gray-700",
ghost: "hover:bg-gray-100 text-gray-700",
destructive: "bg-red-600 text-white hover:bg-red-700",
},
size: {
sm: "h-8 px-3 text-xs",
md: "h-10 px-4 text-sm",
lg: "h-12 px-6 text-base",
icon: "h-10 w-10 p-0",
},
},
defaultVariants: { variant: "default", size: "md" },
}
)
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean // Radix Slot Pattern
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return <Comp ref={ref} className={cn(buttonVariants({ variant, size }), className)} {...props} />
}
)
// Nutzung:
<Button>Standard</Button>
<Button variant="outline" size="sm">Outline Small</Button>
<Button variant="destructive">Löschen</Button>
<Button asChild><Link href="/signup">Registrieren</Link></Button>
Radix Slot: <Slot> aus @radix-ui/react-slot ist das gleiche wie asChild — es mergt alle Props auf das Kind-Element. Damit kannst du Button-Styles auf einen <Link> oder <a> anwenden ohne die Semantik zu verlieren.
FocusGlobale Focus-Styles für barrierefreie UIs
// Prompt: "Globale Accessibility-Styles für Radix-Komponenten"
/* globals.css — Konsistente Focus-Sichtbarkeit */
*:focus-visible {
outline: 2px solid rgb(124 58 237); /* violet-700 */
outline-offset: 2px;
}
*:focus:not(:focus-visible) {
outline: none; /* Kein Focus-Ring bei Maus-Klick */
}
/* Radix Portale immer über allem */
[data-radix-popper-content-wrapper] {
z-index: 50 !important;
}
/* Prevent body scroll wenn Dialog offen */
body[data-scroll-locked] {
overflow: hidden;
padding-right: 15px; /* Scrollbar-Kompensation */
}
/* Skip-to-Content Link (WCAG 2.1 Pflicht) */
.skip-link {
position: absolute;
top: -100%;
left: 50%;
transform: translateX(-50%);
background: #7c3aed;
color: white;
padding: 8px 16px;
border-radius: 0 0 8px 8px;
z-index: 9999;
font-weight: 600;
}
.skip-link:focus {
top: 0; /* Sichtbar bei Tastatur-Navigation */
}
// tailwind.config.js — tailwindcss-animate Plugin:
const { default: flattenColorPalette } = require("tailwindcss/lib/util/flattenColorPalette")
module.exports = {
plugins: [require("tailwindcss-animate")],
// Stellt animate-in, animate-out, fade-in-0, zoom-in-95, etc. bereit
}
WCAG 2.1 Pflicht-Check: Auch mit Radix musst du sicherstellen: (1) Farbkontrast min. 4.5:1, (2) Skip-to-Content Link vorhanden, (3) Bilder haben alt-Text, (4) Formulare haben Labels. Radix löst die Interaktions-Accessibility — visuelles Design liegt bei dir.
PopoverPopover und Tooltip: Positioning Engine
// Prompt: "Popover mit Arrow und automatischer Kollisions-Erkennung"
import * as Popover from '@radix-ui/react-popover'
// Radix nutzt Floating UI intern für Positioning
<Popover.Root>
<Popover.Trigger asChild>
<Button variant="outline">Weitere Infos</Button>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
side="bottom" // Bevorzugte Seite (kollision = auto-flip)
sideOffset={8} // Abstand zum Trigger
align="start" // start | center | end
avoidCollisions // Viewport-Grenzen respektieren (Standard: true)
className="w-80 rounded-xl bg-white shadow-xl border border-gray-200 p-4
data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95
data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2
data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2"
>
<Popover.Arrow className="fill-white drop-shadow-sm" />
<div className="text-sm text-gray-600">
<h4 className="font-semibold text-gray-900 mb-1">Kontextinfo</h4>
<p>Radix Floating UI passt die Position automatisch an den Viewport an.</p>
</div>
<Popover.Close className="absolute top-2 right-2 rounded-sm opacity-70 hover:opacity-100">
<X className="h-4 w-4" />
</Popover.Close>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
// Tooltip (einfacher, kein Close-Button nötig):
import * as Tooltip from '@radix-ui/react-tooltip'
<Tooltip.Provider delayDuration={400}>
<Tooltip.Root>
<Tooltip.Trigger asChild><Button variant="ghost" size="icon"><Info /></Button></Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content
className="bg-gray-900 text-white text-xs rounded-lg px-3 py-1.5
animate-in fade-in-0 zoom-in-95"
sideOffset={4}
>
Hilfetext für dieses Feature
<Tooltip.Arrow className="fill-gray-900" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
Claude Code Prompt-Tipp: "Erstelle eine vollständig barrierefreie [Komponente] mit Radix UI, Tailwind CSS, WAI-ARIA Labels, Keyboard Navigation und Focus Management. Nutze CVA für Varianten und asChild für das Slot-Pattern."
Barrierefreie React-Apps schneller entwickeln
Claude Code kennt alle Radix Primitives, WAI-ARIA Patterns und Tailwind-Animationen. Generiere vollständig barrierefreie UI-Komponenten in Sekunden — keine Accessibility-Kenntnisse erforderlich.
Jetzt kostenlos starten →