</>
{ }
Radix UI · Headless Components · React · Accessibility
Radix UI: Headless Komponenten
mit voller Accessibility

Eigene UI-Komponenten ohne WAI-ARIA richtig zu implementieren, kostet Wochen. Dialog, Dropdown, Tooltip und Select korrekt mit Keyboard-Navigation, Fokus-Trapping und Screenreader-Unterstützung zu bauen ist komplexer als die meisten Entwickler ahnen. Radix UI löst genau dieses Problem.

16 Min. Lesezeit Radix UI · WAI-ARIA · Tailwind CSS · Headless Components React 18+ · Radix UI 2.x

1. Warum Accessibility ohne Radix UI so schwer ist

Ein zugänglicher Dialog ist kein einfaches Modal mit position: fixed. Korrekte Accessibility erfordert: Fokus muss beim Öffnen in den Dialog springen, Tab-Navigation muss innerhalb des Dialogs gefangen bleiben (Focus Trapping), beim Schließen muss der Fokus zum auslösenden Element zurückkehren, Escape muss den Dialog schließen, das Hintergrund-Dokument muss für Screenreader versteckt werden (aria-hidden), und die Rolle dialog mit aria-modal="true" und aria-labelledby muss korrekt gesetzt werden. All das ist nach WAI-ARIA-Authoring-Patterns definiert – und alles zu implementieren dauert Tage, nicht Stunden.

Radix UI implementiert all das in seinen Primitives. Die Bibliothek besteht aus einzeln installierbaren Packages pro Komponente (@radix-ui/react-dialog, @radix-ui/react-dropdown-menu, etc.), die vollständige WAI-ARIA-Compliance mitbringen. Kein Styling, keine Meinung über Farben oder Abstände – nur Verhalten, Accessibility und State-Management. Das ist der Kern des Headless-Ansatzes: Trennung von Verhalten und Erscheinung. Wer Radix UI nutzt, bekommt die schwierige Accessibility-Implementierung geschenkt und investiert die Zeit stattdessen in Design und Business-Logik.

Die Verbreitung von Radix UI spiegelt diesen Vorteil wider: shadcn/ui, die beliebteste React-Komponentensammlung 2026, basiert vollständig auf Radix UI Primitives. Tremor, Radix Themes und zahlreiche andere Komponentenbibliotheken nutzen die gleichen Primitives als Fundament. Das bedeutet: Die Investment in das Verstehen von Radix UI zahlt sich auch dann aus, wenn man auf bereits gestylte Komponentenbibliotheken umsteigt oder eigene Systeme aufbaut.

2. Das Headless-Konzept: Verhalten ohne Stil

Headless Komponenten liefern State, Verhalten und Accessibility-Attribute – aber kein CSS. Sie unterscheiden sich damit fundamental von klassischen UI-Libraries wie Material UI oder Ant Design, die Komponenten mit einem festen Design-System mitbringen. Der Vorteil: Radix UI-Komponenten sehen aus, wie man sie designt – nicht wie eine generische Library. Das ist besonders wichtig für Projekte mit starken Design-Anforderungen oder einem eigenen Design-System.

Technisch funktioniert Radix UI über das Compound Component Pattern: Eine Komponente wie Dialog.Root stellt State über React Context zur Verfügung, und alle Kind-Komponenten (Dialog.Trigger, Dialog.Content, Dialog.Title) sind an diesem Context angeschlossen. Man muss nichts manuell verknüpfen – die Primitive wissen, wie sie zusammengehören. Die Accessibility-Attribute werden automatisch gesetzt: aria-expanded, aria-controls, aria-labelledby, role, tabIndex und alle anderen ARIA-Attribute entstehen automatisch aus dem State der Komponente.


// Basic Radix UI Dialog — full accessibility with zero custom ARIA code
import * as Dialog from '@radix-ui/react-dialog'

export function ConfirmDialog({ onConfirm, children }) {
  return (
    <Dialog.Root>
      {/* Trigger receives aria-haspopup, aria-expanded automatically */}
      <Dialog.Trigger asChild>
        <button className="rounded-lg bg-red-600 px-4 py-2 text-white hover:bg-red-700">
          Delete Item
        </button>
      </Dialog.Trigger>

      {/* Portal renders outside the current DOM tree to avoid z-index issues */}
      <Dialog.Portal>
        {/* Overlay covers background, aria-hidden on body automatically set */}
        <Dialog.Overlay className="fixed inset-0 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />

        {/* Content has role="dialog", aria-modal="true", focus trapped inside */}
        <Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md rounded-2xl bg-white p-6 shadow-xl focus:outline-none">
          {/* Title is auto-connected via aria-labelledby */}
          <Dialog.Title className="text-lg font-bold text-gray-900">
            Confirm Delete
          </Dialog.Title>
          <Dialog.Description className="mt-2 text-sm text-gray-600">
            This action cannot be undone.
          </Dialog.Description>

          <div className="mt-6 flex gap-3 justify-end">
            {/* Close dismisses dialog and returns focus to trigger */}
            <Dialog.Close asChild>
              <button className="rounded-lg border px-4 py-2 text-sm">Cancel</button>
            </Dialog.Close>
            <button onClick={onConfirm} className="rounded-lg bg-red-600 px-4 py-2 text-sm text-white">
              Delete
            </button>
          </div>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  )
}

3. Dialog: Fokus-Trapping und ARIA-Modal korrekt

Die Radix UI Dialog-Primitive löst alle komplexen Accessibility-Anforderungen automatisch: Beim Öffnen springt der Fokus in den Dialog-Content, genauer gesagt auf das erste fokussierbare Element oder auf das Content-Element selbst. Tab-Navigation bleibt innerhalb des Dialogs (Focus Trapping) – kein Tab-Stop verlässt den Dialog, solange er offen ist. Beim Schließen – sei es per Escape-Taste, per Click auf den Close-Button oder per Click auf das Overlay – kehrt der Fokus zum ursprünglichen Trigger-Element zurück.

Das asChild-Prop ist ein mächtiges Muster in Radix UI: Statt ein eigenes DOM-Element zu rendern, übergibt die Primitive ihre Props und Event-Handler an das direkte Child-Element. Dialog.Trigger asChild mit einem <button> erzeugt keinen verschachtelten Button-in-Button, sondern übergibt die Trigger-Funktionalität direkt an den Button. Das ermöglicht vollständige Kontrolle über das DOM-Element ohne Accessibility-Kompromisse. Das asChild-Muster funktioniert mit allen Radix UI-Primitives und ist der empfohlene Weg, eigene Styled-Komponenten als Trigger zu verwenden.

Ein zugängliches Dropdown-Menü erfordert nach WAI-ARIA: Öffnen mit Enter oder Space auf dem Trigger, Navigation durch Items mit Pfeiltasten (ArrowUp, ArrowDown), Aktivierung mit Enter oder Space, Schließen mit Escape, Type-Ahead-Suche (Buchstaben drücken springt zu Menü-Items, die mit diesem Buchstaben beginnen). Das sind acht verschiedene Keyboard-Interaktionen, die Radix UI's DropdownMenu-Primitive vollständig implementiert.

Besonders hilfreich sind die vordefinierten Item-Typen: DropdownMenu.Item für normale Items, DropdownMenu.CheckboxItem für Toggle-Items mit aria-checked, DropdownMenu.RadioGroup und DropdownMenu.RadioItem für Einzel-Auswahl mit role="menuitemradio". DropdownMenu.Separator rendert eine visuelle Trennlinie mit der korrekten role="separator". Submenüs werden über DropdownMenu.Sub, DropdownMenu.SubTrigger und DropdownMenu.SubContent realisiert, mit automatischer Keyboard-Navigation durch ArrowRight und ArrowLeft. All das ohne eine Zeile eigenen Accessibility-Code.


// DropdownMenu with keyboard navigation, checkboxes and separators
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { useState } from 'react'

export function UserMenu({ user }) {
  const [showNotifications, setShowNotifications] = useState(true)

  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        <button className="flex items-center gap-2 rounded-full p-1 hover:bg-gray-100">
          <img src={user.avatar} alt="" className="h-8 w-8 rounded-full" />
          <span className="text-sm font-medium">{user.name}</span>
        </button>
      </DropdownMenu.Trigger>

      <DropdownMenu.Portal>
        <DropdownMenu.Content
          className="z-50 min-w-48 rounded-xl border bg-white p-1 shadow-lg"
          sideOffset={5}
          align="end"
        >
          {/* Regular item — activated by Enter, Space, or click */}
          <DropdownMenu.Item className="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm outline-none focus:bg-sky-50 focus:text-sky-700 data-[highlighted]:bg-sky-50">
            Profile Settings
          </DropdownMenu.Item>

          <DropdownMenu.Separator className="my-1 h-px bg-gray-100" />

          {/* CheckboxItem has aria-checked and renders checkmark automatically */}
          <DropdownMenu.CheckboxItem
            className="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm outline-none focus:bg-sky-50 data-[highlighted]:bg-sky-50"
            checked={showNotifications}
            onCheckedChange={setShowNotifications}
          >
            <DropdownMenu.ItemIndicator>✓</DropdownMenu.ItemIndicator>
            Email Notifications
          </DropdownMenu.CheckboxItem>

          <DropdownMenu.Separator className="my-1 h-px bg-gray-100" />

          {/* Destructive item — semantic styling only, no special ARIA needed */}
          <DropdownMenu.Item className="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-red-600 outline-none focus:bg-red-50 data-[highlighted]:bg-red-50">
            Sign Out
          </DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  )
}

5. Tooltip und Popover: Hover, Focus und Touch

Tooltips sind deceptively complex: Sie müssen bei Hover angezeigt werden, bei Fokus per Tastatur angezeigt werden, auf Touch-Geräten nach Tap sichtbar bleiben, und sie dürfen niemals den einzigen Weg darstellen, wichtige Informationen zu erhalten (WCAG 2.1 Criterion 1.4.13). Radix UI's Tooltip-Primitive implementiert alle diese Regeln: automatische Verzögerung beim Hover, sofortige Anzeige bei Fokus, role="tooltip" und aria-describedby-Verknüpfung zwischen Trigger und Content, und Escape-Taste schließt den Tooltip.

Der Unterschied zwischen Tooltip und Popover in Radix UI ist konzeptionell wichtig: Ein Tooltip zeigt ergänzende Information und ist nicht interaktiv. Ein Popover kann interaktive Elemente enthalten (Links, Buttons, Formulare). Radix UI's Popover-Primitive setzt dementsprechend role="dialog" statt role="tooltip" und implementiert Focus-Management: Beim Öffnen springt der Fokus in den Popover-Content, beim Schließen kehrt er zum Trigger zurück. Ein Tooltip-Element mit interaktivem Inhalt zu bauen wäre ein Accessibility-Fehler – mit Radix UI ist diese semantische Unterscheidung klar.

6. Select: warum man kein natives select ersetzen sollte

Das native HTML <select>-Element ist per se vollständig zugänglich – es kommt mit Keyboard-Navigation, Screenreader-Unterstützung und mobilem Systemdialog. Warum gibt es trotzdem Radix UI's Select-Primitive? Weil natives select kaum anpassbar ist: Custom Icons, mehrzeilige Optionen, Optionen-Gruppen mit Trennlinien, Suche innerhalb des Dropdowns, und komplexe Option-Rendering – all das ist mit einem nativen select nicht oder kaum möglich.

Radix UI Select reimplementiert die vollständige Accessibility des nativen Elements: role="combobox" auf dem Trigger, role="listbox" auf dem Content, role="option" auf jedem Item, ArrowUp/ArrowDown-Navigation, Home/End, Buchstaben-Typeahead, und korrekte aria-selected auf dem aktiven Item. Auf mobilen Geräten rendert Radix UI Select zusätzlich ein verstecktes natives select, damit Formulare korrekt mit dem System-Keyboard interagieren. Das ist die Art von Detail, die man bei einer manuellen Implementierung leicht übersieht.

7. Radix UI mit Tailwind CSS stylen: das data-state-Muster

Radix UI kommuniziert den aktuellen State von Komponenten über Data-Attribute auf den DOM-Elementen. data-state="open" oder data-state="closed" auf einem Dialog-Overlay, data-state="checked" auf einem Checkbox-Item, data-highlighted auf dem aktuell fokussierten Menü-Item. Mit Tailwind CSS v3's data-[state=open]:animate-in-Syntax oder Tailwind CSS v4's nativer Data-Attribute-Unterstützung kann man direkt auf diese States reagieren und CSS-Animationen, Farb-Änderungen und Layout-Transformationen triggern.

Das macht die Integration von Radix UI mit Tailwind besonders elegant: Statt manuell State-abhängige Klassen zu berechnen und in JSX zu injizieren, deklariert man in der className direkt, wie ein Element in jedem State aussehen soll. data-[state=checked]:bg-sky-600 auf einem Checkbox-Trigger macht die Farbe blau, wenn der Checkbox aktiviert ist. data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed macht deaktivierte Items visuell disabled. Der State kommt von Radix UI, die Visualisierung kommt von Tailwind – eine klare Trennung der Verantwortlichkeiten.


// Radix UI + Tailwind: data-state styling pattern for animated transitions
import * as Dialog from '@radix-ui/react-dialog'

// tailwind.config.ts — enable data-state variants (Tailwind v3)
// plugins: [require('tailwindcss-animate')]
// Custom variants for data attributes are in Tailwind v4 by default

export function AnimatedDialog({ trigger, title, children }) {
  return (
    <Dialog.Root>
      <Dialog.Trigger asChild>{trigger}</Dialog.Trigger>
      <Dialog.Portal>
        {/* data-[state=open] and data-[state=closed] control enter/exit animations */}
        <Dialog.Overlay
          className="
            fixed inset-0 bg-black/40
            data-[state=open]:animate-in data-[state=open]:fade-in-0
            data-[state=closed]:animate-out data-[state=closed]:fade-out-0
            duration-200
          "
        />
        <Dialog.Content
          className="
            fixed left-1/2 top-1/2 w-full max-w-lg -translate-x-1/2 -translate-y-1/2
            rounded-2xl bg-white p-6 shadow-2xl
            data-[state=open]:animate-in data-[state=open]:fade-in-0
            data-[state=open]:zoom-in-95 data-[state=open]:slide-in-from-bottom-4
            data-[state=closed]:animate-out data-[state=closed]:fade-out-0
            data-[state=closed]:zoom-out-95 data-[state=closed]:slide-out-to-bottom-4
            duration-200
          "
        >
          <Dialog.Title className="text-xl font-bold text-gray-900">{title}</Dialog.Title>
          <div className="mt-4">{children}</div>
          <Dialog.Close asChild>
            <button
              className="absolute right-4 top-4 rounded-lg p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
              aria-label="Close dialog"
            >
              ✕
            </button>
          </Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  )
}

8. Composing: eigene Komponenten-Bibliothek aufbauen

Der eigentliche Power-Anwendungsfall von Radix UI ist der Aufbau einer eigenen, markenkonsistenten Komponentenbibliothek auf Basis der Primitives. Anstatt Dialog, Dropdown und Select jedes Mal von Grund auf zu konfigurieren, erstellt man einmal gestylte Wrapper-Komponenten, die Radix UI-Primitives verwenden und die Design-System-Tokens einbetten. Das Ergebnis ist eine Bibliothek, die überall in der Anwendung konsistent aussieht, vollständig zugänglich ist und sich wie eine first-party Komponente anfühlt – weil sie es ist.

shadcn/ui zeigt diesen Ansatz in seiner reinsten Form: Die Komponenten werden nicht als npm-Package installiert, sondern als Quellcode in das Projekt kopiert, der dann nach Bedarf angepasst wird. Jede Komponente nutzt Radix UI Primitives, ist mit Tailwind gestylt und enthält TypeScript-Props-Interfaces. Das ermöglicht vollständige Anpassung ohne Framework-Lock-in. Teams, die diesen Ansatz verfolgen, haben die vollständige Kontrolle über jede Komponente, ohne die Accessibility-Grundlage von Radix UI zu verlieren.

9. Radix UI vs. Headless UI vs. Ariakit

Es gibt mehrere Headless-Component-Libraries für React, die ähnliche Ziele verfolgen. Headless UI von Tailwind Labs ist die direkteste Alternative zu Radix UI: fokussiert auf Tailwind-Integration, kleineres Komponentenset, aber sehr poliertes Developer-Experience. Ariakit (früher Reakit) ist die akademischste Lösung mit dem stärksten Fokus auf WAI-ARIA-Konformität und einer kompositionsfähigeren API. Radix UI liegt zwischen beiden: umfangreiches Komponentenset, ausgereiftes API-Design und die stärkste Community durch shadcn/ui.

Eigenschaft Radix UI Headless UI Ariakit
Komponentenumfang Sehr groß (30+) Mittel (15+) Groß (25+)
shadcn/ui Integration Basis-Library Nicht integriert Nicht integriert
WAI-ARIA-Konformität Sehr hoch Hoch Sehr hoch
API-Stabilität Sehr stabil Stabil Wechselnd
Tailwind-Freundlichkeit data-state-Pattern Render Props Props nötig

Die Empfehlung für 2026: Radix UI ist die sicherste Wahl für neue Projekte, die eine eigene Komponentenbibliothek aufbauen wollen. Das Ökosystem, die Stabilität und die shadcn/ui-Integration geben einen erheblichen Entwicklungsvorsprung. Headless UI ist die bessere Wahl, wenn das Projekt bereits stark auf Tailwind aufbaut und ein kleineres, fokussiertes Komponentenset ausreicht. Ariakit für Teams, die maximale API-Flexibilität über Komfort stellen.

Mironsoft

React Komponentenbibliothek, Accessibility und Design System Entwicklung

Eigene accessible Komponentenbibliothek aufbauen?

Wir bauen auf Basis von Radix UI und Tailwind CSS eine markenkonsistente, vollständig zugängliche Komponentenbibliothek für euer React-Projekt – mit Design Tokens, vollständiger WCAG-Compliance und Dokumentation.

Design System

Komponentenbibliothek mit Radix UI Primitives, Tailwind und Design Tokens aufbauen

Accessibility-Audit

Bestehende UI-Komponenten auf WCAG 2.2-Konformität prüfen und mit Radix UI nachrüsten

Implementierung

Dialog, Dropdown, Select, Tooltip und Popover korrekt implementieren mit vollständiger Tests

10. Zusammenfassung

Radix UI ist die pragmatischste Antwort auf das Accessibility-Problem in React-Anwendungen. Korrekte ARIA-Implementierungen für Dialog, Dropdown, Select und Tooltip zu schreiben erfordert tiefes Wissen der WAI-ARIA-Authoring-Patterns, extensives Testing mit Screenreadern und Keyboard-Navigation und laufende Wartung bei Browser-Updates. Radix UI trägt diese Verantwortung für Teams, die dadurch ihre Zeit in Design, Business-Logik und Features investieren können.

Das data-state-Styling-Muster in Kombination mit Tailwind CSS macht Radix UI zu einem vollständigen Fundament für jede Design-System-Entwicklung. Die Trennung zwischen Verhalten (Radix) und Erscheinung (Tailwind) ist klar, wartbar und skaliert von kleinen Projekten bis zu großen Komponentenbibliotheken. Teams, die Radix UI als Basis verwenden und darauf eine markenkonsistente Schicht aufbauen, bekommen das Beste aus beiden Welten: vollständige Design-Freiheit und vollständige Accessibility-Compliance ohne Kompromisse.

Radix UI Headless Components — Das Wichtigste auf einen Blick

WAI-ARIA out of the box

Focus Trapping, Keyboard-Navigation, ARIA-Attribute und Fokus-Rückkehr automatisch implementiert. Kein eigener Accessibility-Code nötig.

asChild Pattern

Radix UI-Primitives delegieren ihre Props an eigene DOM-Elemente. Vollständige DOM-Kontrolle ohne Accessibility-Kompromisse.

data-state Styling

State als Data-Attribute (data-state="open", data-highlighted). Tailwind-Klassen wie data-[state=open]:animate-in für deklarative State-Styles.

shadcn/ui Ökosystem

shadcn/ui basiert auf Radix UI Primitives. Investment in Radix zahlt sich im gesamten shadcn/ui-Ökosystem aus.

11. FAQ: Radix UI Headless Komponenten

1Radix UI vs. shadcn/ui: Was ist der Unterschied?
Radix UI: unstyled Primitives (Verhalten, Accessibility). shadcn/ui: Tailwind-gestylte Komponenten auf Basis von Radix UI, direkt als Quellcode im Projekt – keine npm-Dependency.
2Jede Komponente einzeln installieren?
Ja. Eigene npm-Packages pro Primitive: @radix-ui/react-dialog etc. Ermöglicht Tree-Shaking – nur installieren, was wirklich gebraucht wird.
3Radix UI auf Accessibility testen?
Playwright CT + axe-core für automatische WCAG-Scans. Manuell mit Screenreader (NVDA, VoiceOver). Tab-Navigation, Escape und Fokus-Rückkehr prüfen.
4Radix UI mit Next.js Server Components?
Radix UI Primitives sind Client Components. In 'use client'-Dateien verwenden oder als Child einer Client-Component-Boundary. Nicht direkt in Server Components nutzbar.
5Was macht das asChild-Prop?
Delegiert Props, Events und ARIA-Attribute ans Child-Element statt ein eigenes DOM-Element zu rendern. Vermeidet Button-in-Button, gibt vollständige DOM-Kontrolle.
6Radix UI ohne Tailwind CSS nutzbar?
Ja. Framework-agnostisch. CSS-Modules, SCSS, Styled Components oder plain CSS. [data-state='open'] { ... } als CSS-Attribut-Selektor statt Tailwind-Varianten.
7Welche Primitives gibt es in Radix UI?
Über 30: Dialog, Dropdown Menu, Select, Tooltip, Popover, Accordion, Tabs, Checkbox, Radio Group, Switch, Slider, Progress, Toast, Alert Dialog und viele mehr.
8Animationen mit Radix UI?
data-state="open"/"closed" als Hooks für CSS-Animationen. Tailwind: data-[state=open]:animate-in, data-[state=closed]:animate-out. Deklarativ, kein manueller State nötig.
9Ist Radix UI kostenlos?
Ja, vollständig. MIT-Lizenz. Primitives und Radix Themes beide kostenlos. Kein Pro-Tier, keine Usage-Limits.
10Warum nicht Material UI oder Ant Design?
Festes Design-System – schwer zu überschreiben für eigene Designs. Radix UI gibt vollständige Design-Freiheit mit gleicher oder besserer Accessibility. Langfristig wartbarer bei eigenem Design.