# 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

```bash
# 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/primitives
```

---

## Dialog (Modal)

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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:

```bash
# 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
```

```typescript
// 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

```typescript
// 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:

1. **Volle Accessibility**: WAI-ARIA compliant out-of-the-box
2. **Styling-Freiheit**: Headless Architecture, kein UI Lock-in
3. **Composable**: Flexible, zusammensetzbare APIs
4. **shadcn/ui Basis**: Foundation für moderne Design Systems

Für barrierefreie React-Anwendungen ist Radix UI die beste Grundlage.

---

## Bildprompts

1. "Accessibility symbols surrounding React components, inclusive design concept"
2. "Unstyled building blocks transforming into styled components, headless architecture"
3. "Keyboard navigation flow through UI components, accessibility visualization"

---

## Quellen

- [Radix UI Documentation](https://www.radix-ui.com/)
- [Radix Primitives](https://www.radix-ui.com/primitives)
- [shadcn/ui](https://ui.shadcn.com/)
- [WAI-ARIA Design Patterns](https://www.w3.org/WAI/ARIA/apg/patterns/)
