Menu
Back to Blog
1 min read
Web Development

Radix 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

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/primitives

Dialog (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:

  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