1 min read
Web DevelopmentRadix UI: Accessible Headless Components für React
Radix UI Primitives für barrierefreie React-Komponenten. Headless Architecture, WAI-ARIA Compliance und shadcn/ui Integration.
Radix UIHeadless ComponentsAccessibilityWAI-ARIAReact Componentsshadcn/ui

Radix UI: Accessible Headless Components für React
Meta-Description: Radix UI Primitives für barrierefreie React-Komponenten. Headless Architecture, WAI-ARIA Compliance und shadcn/ui Integration.
Keywords: Radix UI, Headless Components, Accessibility, WAI-ARIA, React Components, shadcn/ui, Unstyled Components
Einführung
Radix UI ist eine Headless Component Library für React. Statt vorgestylter Komponenten liefert sie zugängliche, ungestylte Bausteine – perfekte Grundlage für Design Systems mit voller Accessibility out-of-the-box.
Warum Radix UI?
┌─────────────────────────────────────────────────────────────┐
│ RADIX UI VORTEILE │
├─────────────────────────────────────────────────────────────┤
│ │
│ Accessibility (A11Y): │
│ ├── WAI-ARIA Design Patterns │
│ ├── Keyboard Navigation │
│ ├── Focus Management │
│ ├── Screen Reader Support │
│ └── Reduced Motion Support │
│ │
│ Headless Architecture: │
│ ├── Zero Styles (volle Kontrolle) │
│ ├── Jedes Styling möglich (CSS, Tailwind, CSS-in-JS) │
│ ├── Kein UI Lock-in │
│ └── Perfekt für Design Systems │
│ │
│ Developer Experience: │
│ ├── Composable APIs │
│ ├── TypeScript Support │
│ ├── SSR Compatible │
│ └── Tree-Shakeable │
│ │
└─────────────────────────────────────────────────────────────┘Installation
# Einzelne Primitives installieren
npm install @radix-ui/react-dialog
npm install @radix-ui/react-dropdown-menu
npm install @radix-ui/react-tabs
npm install @radix-ui/react-tooltip
# Oder alle Primitives
npm install @radix-ui/primitivesDialog (Modal)
import * as Dialog from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
function ConfirmDialog({
trigger,
title,
description,
onConfirm
}: {
trigger: React.ReactNode;
title: string;
description: string;
onConfirm: () => void;
}) {
return (
<Dialog.Root>
<Dialog.Trigger asChild>
{trigger}
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50 animate-fade-in" />
<Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg p-6 w-[90vw] max-w-md shadow-xl animate-scale-in">
<Dialog.Title className="text-lg font-semibold">
{title}
</Dialog.Title>
<Dialog.Description className="text-gray-500 mt-2">
{description}
</Dialog.Description>
<div className="flex justify-end gap-3 mt-6">
<Dialog.Close asChild>
<button className="px-4 py-2 rounded border hover:bg-gray-100">
Abbrechen
</button>
</Dialog.Close>
<Dialog.Close asChild>
<button
onClick={onConfirm}
className="px-4 py-2 rounded bg-red-500 text-white hover:bg-red-600"
>
Löschen
</button>
</Dialog.Close>
</div>
<Dialog.Close asChild>
<button
className="absolute top-4 right-4 p-1 rounded-full hover:bg-gray-100"
aria-label="Schließen"
>
<X className="w-4 h-4" />
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
// Verwendung
<ConfirmDialog
trigger={<button>Löschen</button>}
title="Eintrag löschen?"
description="Diese Aktion kann nicht rückgängig gemacht werden."
onConfirm={() => deleteItem(id)}
/>Dropdown Menu
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { Check, ChevronRight, Circle } from 'lucide-react';
function UserMenu({ user }: { user: User }) {
const [bookmarksChecked, setBookmarksChecked] = useState(true);
const [person, setPerson] = useState('pedro');
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button className="flex items-center gap-2 p-2 rounded-full hover:bg-gray-100">
<img
src={user.avatar}
alt={user.name}
className="w-8 h-8 rounded-full"
/>
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
className="min-w-[220px] bg-white rounded-md shadow-lg p-1 animate-slide-down"
sideOffset={5}
>
{/* Regular Items */}
<DropdownMenu.Item className="px-3 py-2 rounded cursor-pointer hover:bg-gray-100 outline-none">
Profil
</DropdownMenu.Item>
<DropdownMenu.Item className="px-3 py-2 rounded cursor-pointer hover:bg-gray-100 outline-none">
Einstellungen
</DropdownMenu.Item>
<DropdownMenu.Separator className="h-px bg-gray-200 my-1" />
{/* Checkbox Item */}
<DropdownMenu.CheckboxItem
className="px-3 py-2 rounded cursor-pointer hover:bg-gray-100 outline-none flex items-center"
checked={bookmarksChecked}
onCheckedChange={setBookmarksChecked}
>
<DropdownMenu.ItemIndicator className="mr-2">
<Check className="w-4 h-4" />
</DropdownMenu.ItemIndicator>
Lesezeichen anzeigen
</DropdownMenu.CheckboxItem>
<DropdownMenu.Separator className="h-px bg-gray-200 my-1" />
{/* Radio Group */}
<DropdownMenu.Label className="px-3 py-1 text-xs text-gray-500">
Personen
</DropdownMenu.Label>
<DropdownMenu.RadioGroup value={person} onValueChange={setPerson}>
<DropdownMenu.RadioItem
className="px-3 py-2 rounded cursor-pointer hover:bg-gray-100 outline-none flex items-center"
value="pedro"
>
<DropdownMenu.ItemIndicator className="mr-2">
<Circle className="w-2 h-2 fill-current" />
</DropdownMenu.ItemIndicator>
Pedro
</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem
className="px-3 py-2 rounded cursor-pointer hover:bg-gray-100 outline-none flex items-center"
value="maria"
>
<DropdownMenu.ItemIndicator className="mr-2">
<Circle className="w-2 h-2 fill-current" />
</DropdownMenu.ItemIndicator>
Maria
</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
<DropdownMenu.Separator className="h-px bg-gray-200 my-1" />
{/* Sub Menu */}
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger className="px-3 py-2 rounded cursor-pointer hover:bg-gray-100 outline-none flex items-center justify-between">
Mehr
<ChevronRight className="w-4 h-4" />
</DropdownMenu.SubTrigger>
<DropdownMenu.Portal>
<DropdownMenu.SubContent
className="min-w-[180px] bg-white rounded-md shadow-lg p-1"
sideOffset={2}
>
<DropdownMenu.Item className="px-3 py-2 rounded cursor-pointer hover:bg-gray-100 outline-none">
Hilfe
</DropdownMenu.Item>
<DropdownMenu.Item className="px-3 py-2 rounded cursor-pointer hover:bg-gray-100 outline-none">
Über uns
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Portal>
</DropdownMenu.Sub>
<DropdownMenu.Separator className="h-px bg-gray-200 my-1" />
{/* Destructive Item */}
<DropdownMenu.Item className="px-3 py-2 rounded cursor-pointer hover:bg-red-100 text-red-600 outline-none">
Abmelden
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}Tabs
import * as Tabs from '@radix-ui/react-tabs';
function SettingsTabs() {
return (
<Tabs.Root defaultValue="account" className="w-full max-w-lg">
<Tabs.List className="flex border-b" aria-label="Einstellungen">
<Tabs.Trigger
value="account"
className="px-4 py-2 border-b-2 border-transparent data-[state=active]:border-blue-500 data-[state=active]:text-blue-600 hover:text-gray-700"
>
Account
</Tabs.Trigger>
<Tabs.Trigger
value="password"
className="px-4 py-2 border-b-2 border-transparent data-[state=active]:border-blue-500 data-[state=active]:text-blue-600 hover:text-gray-700"
>
Passwort
</Tabs.Trigger>
<Tabs.Trigger
value="notifications"
className="px-4 py-2 border-b-2 border-transparent data-[state=active]:border-blue-500 data-[state=active]:text-blue-600 hover:text-gray-700"
>
Benachrichtigungen
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="account" className="p-4">
<h3 className="font-semibold mb-4">Account-Einstellungen</h3>
<form>
<label className="block mb-2">
Name
<input type="text" className="w-full border rounded px-3 py-2 mt-1" />
</label>
<label className="block mb-4">
E-Mail
<input type="email" className="w-full border rounded px-3 py-2 mt-1" />
</label>
<button className="px-4 py-2 bg-blue-500 text-white rounded">
Speichern
</button>
</form>
</Tabs.Content>
<Tabs.Content value="password" className="p-4">
<h3 className="font-semibold mb-4">Passwort ändern</h3>
{/* Passwort Form */}
</Tabs.Content>
<Tabs.Content value="notifications" className="p-4">
<h3 className="font-semibold mb-4">Benachrichtigungen</h3>
{/* Notification Settings */}
</Tabs.Content>
</Tabs.Root>
);
}Tooltip
import * as Tooltip from '@radix-ui/react-tooltip';
function IconButton({
icon,
label,
onClick
}: {
icon: React.ReactNode;
label: string;
onClick: () => void;
}) {
return (
<Tooltip.Provider delayDuration={200}>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button
onClick={onClick}
className="p-2 rounded-full hover:bg-gray-100"
aria-label={label}
>
{icon}
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content
className="bg-gray-900 text-white px-3 py-1.5 rounded text-sm animate-fade-in"
sideOffset={5}
>
{label}
<Tooltip.Arrow className="fill-gray-900" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}
// Verwendung
<IconButton
icon={<Settings className="w-5 h-5" />}
label="Einstellungen"
onClick={() => openSettings()}
/>Accordion
import * as Accordion from '@radix-ui/react-accordion';
import { ChevronDown } from 'lucide-react';
interface FAQItem {
question: string;
answer: string;
}
function FAQ({ items }: { items: FAQItem[] }) {
return (
<Accordion.Root type="single" collapsible className="w-full max-w-lg">
{items.map((item, index) => (
<Accordion.Item
key={index}
value={`item-${index}`}
className="border-b"
>
<Accordion.Header>
<Accordion.Trigger className="flex items-center justify-between w-full py-4 text-left hover:underline group">
{item.question}
<ChevronDown className="w-4 h-4 transition-transform group-data-[state=open]:rotate-180" />
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content className="overflow-hidden data-[state=open]:animate-accordion-down data-[state=closed]:animate-accordion-up">
<div className="pb-4 text-gray-600">
{item.answer}
</div>
</Accordion.Content>
</Accordion.Item>
))}
</Accordion.Root>
);
}Select
import * as Select from '@radix-ui/react-select';
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
interface Option {
value: string;
label: string;
}
function CustomSelect({
options,
value,
onChange,
placeholder = 'Auswählen...'
}: {
options: Option[];
value: string;
onChange: (value: string) => void;
placeholder?: string;
}) {
return (
<Select.Root value={value} onValueChange={onChange}>
<Select.Trigger className="inline-flex items-center justify-between px-4 py-2 border rounded-md bg-white min-w-[180px] hover:bg-gray-50">
<Select.Value placeholder={placeholder} />
<Select.Icon>
<ChevronDown className="w-4 h-4" />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className="bg-white rounded-md shadow-lg border overflow-hidden">
<Select.ScrollUpButton className="flex items-center justify-center h-6 bg-white cursor-default">
<ChevronUp className="w-4 h-4" />
</Select.ScrollUpButton>
<Select.Viewport className="p-1">
{options.map((option) => (
<Select.Item
key={option.value}
value={option.value}
className="flex items-center px-8 py-2 rounded cursor-pointer hover:bg-gray-100 outline-none relative"
>
<Select.ItemIndicator className="absolute left-2">
<Check className="w-4 h-4" />
</Select.ItemIndicator>
<Select.ItemText>{option.label}</Select.ItemText>
</Select.Item>
))}
</Select.Viewport>
<Select.ScrollDownButton className="flex items-center justify-center h-6 bg-white cursor-default">
<ChevronDown className="w-4 h-4" />
</Select.ScrollDownButton>
</Select.Content>
</Select.Portal>
</Select.Root>
);
}shadcn/ui Integration
shadcn/ui baut auf Radix Primitives und Tailwind CSS:
# shadcn/ui initialisieren
npx shadcn@latest init
# Komponenten hinzufügen
npx shadcn@latest add button
npx shadcn@latest add dialog
npx shadcn@latest add dropdown-menu// components/ui/dialog.tsx (generiert von shadcn/ui)
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { cn } from '@/lib/utils';
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/80" />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%]',
'w-full max-w-lg rounded-lg bg-white p-6 shadow-lg',
className
)}
{...props}
>
{children}
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
));Accessibility Best Practices
// 1. Keyboard Navigation
// Radix handled automatisch:
// - Tab: Fokus zwischen interaktiven Elementen
// - Enter/Space: Aktivierung
// - Escape: Schließen von Overlays
// - Arrow Keys: Navigation in Listen/Menüs
// 2. ARIA Attributes
// Automatisch gesetzt von Radix:
// - aria-expanded
// - aria-controls
// - aria-labelledby
// - role="dialog", role="menu", etc.
// 3. Focus Management
// Radix handled:
// - Focus Trap in Modals
// - Focus Restore beim Schließen
// - Focus auf erstes interaktives Element
// 4. Reduced Motion
import * as Dialog from '@radix-ui/react-dialog';
// CSS mit prefers-reduced-motion
const styles = `
.dialog-content {
animation: slideIn 200ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
.dialog-content {
animation: none;
}
}
`;Fazit
Radix UI bietet:
- Volle Accessibility: WAI-ARIA compliant out-of-the-box
- Styling-Freiheit: Headless Architecture, kein UI Lock-in
- Composable: Flexible, zusammensetzbare APIs
- shadcn/ui Basis: Foundation für moderne Design Systems
Für barrierefreie React-Anwendungen ist Radix UI die beste Grundlage.
Bildprompts
- "Accessibility symbols surrounding React components, inclusive design concept"
- "Unstyled building blocks transforming into styled components, headless architecture"
- "Keyboard navigation flow through UI components, accessibility visualization"