shadcn/ui und Radix: Barrierefreiheit bei eigenen Komponenten bewahren
Letzte Woche fragte mich ein Kollege: „Warum lässt sich dieser Button nicht mit der Tastatur bedienen?“
Ich war kurz perplex. Wir nutzen doch shadcn/ui – wie kann das passieren? In den DevTools fiel auf: Er hatte Tooltip.Trigger in ein <div> gewickelt – wegen eines eigenen Styles. Genau dort lag das Problem.
Ehrlich gesagt bin ich ähnliche Fallen auch schon reingelaufen. Am Anfang mit shadcn/ui dachte ich, man könne die Komponenten „einfach so anpassen“ – schließlich liegt der Code ja im Projekt. Styling ändern, Tag tauschen, Wrapper drumherum – sieht oft harmlos aus. Bis QA eines Tages meldete: Tastatur bedient nichts, Screenreader liest nichts, der Flow bricht ab.
Dann wurde klar: Die „Freiheit“ von shadcn/ui hat einen Preis. Du bekommst Quellcode, darunter steckt aber Radix’ Barrierefreiheits-Magie. Verändert man das falsch, bricht sie weg.
Dieser Artikel erklärt das Verhältnis von shadcn/ui und Radix – mit Fokus darauf, wie du beim Anpassen Barrierefreiheit erhältst. Danach solltest du wissen, wie asChild funktioniert, wie Fokusmanagement läuft und wie ARIA vererbt wird – und beim nächsten Umbau, was geht und was tabu ist.
shadcn/ui und Radix: Was ist eigentlich was?
Zuerst etwas, das viele missverstehen: shadcn/ui ist kein npm-Paket.
Du installierst es nicht mit npm install @shadcn/ui. Es ist im Kern eine „Code-Distributionsplattform“ – du kopierst Komponentenquellcode ins Projekt, danach gehört er dir. Ändern, löschen – niemand hält dich auf.
Woher kommt die Barrierefreiheit? Von Radix.
Radix UI ist eine „unstyled“ Komponentenbibliothek, auch Primitives genannt. Kein Styling, nur Verhalten – wie Dialoge den Fokus setzen, Dropdown-Menüs Pfeiltasten behandeln, Tooltips vor Screenreadern verborgen bleiben. Alles nach WAI-ARIA, getestet mit NVDA, JAWS und VoiceOver.
shadcn/ui legt Tailwind CSS darüber: schönes Aussehen, Radix-Verhalten darunter. Der Button-Code wirkt wie ein paar Tailwind-Klassen – drin steckt aber Radix-Logik.
Kurz gesagt:
- Radix sorgt für „funktioniert“: aria-Attribute, role, Fokusmanagement, Tastaturnavigation
- shadcn/ui sorgt für „sieht gut aus“: Tailwind-Styling, Design-Konsistenz
Wenn du shadcn/ui-Komponenten änderst, bearbeitest du die Oberfläche – die Logik kommt von Radix. Oberfläche frei, Unterbau falsch angefasst: Ärger.
asChild: Magie oder Falle?
asChild ist ein besonderes Radix-Attribut. Die meisten Radix-Teile unterstützen es.
Beispiel: Tooltip.Trigger rendert standardmäßig ein <button>. Du willst den Tooltip aber an einen Link – dann asChild:
<Tooltip.Trigger asChild>
<a href="/help">Hilfe-Center</a>
</Tooltip.Trigger>
Mit asChild={true} rendert Radix kein eigenes <button>, sondern klont dein Kind und übergibt Verhalten und Attribute. Der Link hat alle Tooltip-Trigger-Funktionen: Hover, Tastaturfokus, korrekte aria-Attribute.
Praktisch – und tückisch.
Wechselst du zu einem nicht fokussierbaren Element, ist Barrierefreiheit weg.
// ❌ Falsches Beispiel
<Tooltip.Trigger asChild>
<div className="my-custom-wrapper">Klick mich</div>
</Tooltip.Trigger>
Ein div ist per Tastatur nicht fokussierbar (ohne tabIndex={0}), reagiert nicht auf Enter/Space. Screenreader sehen keinen Button. Tastatur-Nutzer „erreichen“ den Tooltip nicht.
Radix-Doku: „If you were to switch it to a div, it would no longer be accessible.“
Meist tauscht man nicht direkt gegen div. Häufiger: eigene React-Komponente:
<Tooltip.Trigger asChild>
<MyButton>Klick mich</MyButton>
</Tooltip.Trigger>
Das geht – mit zwei Pflichtregeln:
1. Deine Komponente muss props spreaden
Radix klont das Kind und übergibt Handler, aria-Attribute, ref. Nimmst du die nicht an, bricht die Funktion ab.
// ❌ Falsch: keine props
const MyButton = () => <button className="btn">...</button>
// ✅ Richtig: alle props spreaden
const MyButton = (props) => <button className="btn" {...props}>...</button>
2. Deine Komponente muss forward ref nutzen
Radix greift manchmal direkt auf das DOM zu (Größe, Fokus). Ohne ref gibt es Fehler.
// ❌ Falsch: kein ref
const MyButton = (props) => <button {...props}>...</button>
// ✅ Richtig: forward ref
const MyButton = React.forwardRef((props, ref) => (
<button {...props} ref={ref}>...</button>
))
Diese Regeln gelten nicht nur für Radix – jede „Leaf“-Komponente sollte props und ref durchreichen.
Noch ein Trick: mehrere Radix-Komponenten verschachteln.
<Tooltip.Trigger asChild>
<Dialog.Trigger asChild>
<MyButton>Dialog öffnen</MyButton>
</Dialog.Trigger>
</Tooltip.Trigger>
Ein Button, gleichzeitig Tooltip- und Dialog-Trigger – beides funktioniert.
Fokusmanagement und Tastaturnavigation
Fokus ist der am leichtesten übersehene Teil von Barrierefreiheit.
Viele denken nur ans Styling – Tastatur- und Screenreader-Nutzer hängen am Fokus.
Radix automatisiert viel. Beispiel:
Beim Öffnen eines AlertDialog springt der Fokus auf Cancel.
Bewusst so designed: AlertDialog bestätigt gefährliche Aktionen (Löschen, Beenden). Wahrscheinlichste Aktion ist „Abbrechen“. Fokus auf Cancel – Enter schließt, kein versehentliches Bestätigen.
Liege der Fokus auf Confirm und jemand drückt Enter – gelöscht. Radix folgt WAI-ARIA Authoring Practices; du musst das nicht selbst coden.
Aber: Eigener AlertDialog-Inhalt kann den Fokus verlegen.
Du fügst ein Eingabefeld hinzu:
<AlertDialog.Content>
<AlertDialog.Title>Löschen bestätigen?</AlertDialog.Title>
<AlertDialog.Description>Bitte „DELETE“ eingeben</AlertDialog.Description>
<input placeholder="DELETE eingeben" /> {/* dein Feld */}
<AlertDialog.Cancel>Abbrechen</AlertDialog.Cancel>
<AlertDialog.Action>Bestätigen</AlertDialog.Action>
</AlertDialog.Content>
Wohin geht der Fokus beim Öffnen?
Radix sucht das erste fokussierbare Element. Dein input steht vor Cancel – Fokus landet dort. Mehrere Tabs bis Cancel. Flow unterbrochen.
Lösung: autoFocus setzen oder Reihenfolge anpassen.
<AlertDialog.Content>
<AlertDialog.Title>Löschen bestätigen?</AlertDialog.Title>
<AlertDialog.Description>Bitte „DELETE“ eingeben</AlertDialog.Description>
<AlertDialog.Cancel autoFocus>Abbrechen</AlertDialog.Cancel> {/* Fokus erzwingen */}
<input placeholder="DELETE eingeben" />
<AlertDialog.Action>Bestätigen</AlertDialog.Action>
</AlertDialog.Content>
Cancel nach vorne oder autoFocus – dann bleibt der Fokus richtig.
Tastaturnavigation: ähnliche Fallen.
Tabs: Links/Rechts wechseln Tabs – WAI-ARIA-Standard. Überschreibst du role="tab" beim Styling, bricht die Navigation.
Dropdown Menu: Pfeile wählen, Enter bestätigt, Esc schließt – Radix intern. onClick statt onSelect auf Menüeinträgen kann Tastaturverhalten stören.
Test simpel: Maus weg, nur Tastatur durch den ganzen Flow.
- Tab erreicht die Komponente?
- Pfeiltasten wechseln Optionen?
- Enter löst Aktionen aus?
- Esc schließt Dialoge?
Hakt eine Stelle – Barrierefreiheit kaputt.
Automatische ARIA-Vererbung
Bei ARIA spart Radix viel Arbeit.
Es setzt passende role- und aria-*-Attribute:
- Dialog:
role="dialog"undaria-modal="true" - Tabs.Tab:
role="tab"undaria-selected - Switch:
role="switch"undaria-checked
Radix erledigt das intern.
Eine Sache bleibt bei dir: accessible name für jedes Steuerelement.
Screenreader-Nutzer müssen wissen, was ein Button tut, wie ein Dialog heißt, wofür ein Feld ist. Ohne Namen nur Raten.
Radix bietet Label primitive:
<Label.Root htmlFor="email-input">E-Mail-Adresse</Label.Root>
<Input id="email-input" />
Label.Root verknüpft mit dem input – der Screenreader sagt zuerst „E-Mail-Adresse“, dann den Wert.
Bei eigenen Controls (kein natives input) Name manuell setzen:
<Switch aria-label="Nachtmodus aktivieren" />
<Tabs.Tab aria-label="Produktdetails" />
Oder aria-labelledby mit sichtbarem Text:
<div id="mode-label">Nachtmodus</div>
<Switch aria-labelledby="mode-label" />
Validierung: Screenreader an, einmal durchklicken.
Mac: VoiceOver (Cmd+F5). Windows: NVDA (kostenlos). Klingt es „Button“ statt „Bestellung absenden“ – fehlt der accessible name.
Außerdem: Farbkontrast.
Radix styled nicht – Kontrast ist deine Aufgabe. WCAG: mindestens 4,5:1 (Normaltext) oder 3:1 (großer Text). shadcn/ui-Defaults meist ok; eigene Farben prüfen.
Tool: WebAIM Contrast Checker – Vorder- und Hintergrundfarbe eingeben.
Praxis-Checkliste
Nach jeder Anpassung an shadcn/ui-Komponenten:
asChild
- Ist das Kind fokussierbar? (button/a/input, kein div)
- Spreadet die eigene Komponente alle props?
- Forward ref?
Fokusmanagement
- Landet der Fokus beim Öffnen richtig?
- Kehrt er nach Schließen zum Trigger zurück?
- Bei verschachtelten Fokus-Elementen: sinnvolle Tab-Reihenfolge?
Tastaturnavigation
- Tab erreicht die Komponente?
- Pfeiltasten bei Tabs/Dropdown?
- Enter löst Aktionen aus?
- Esc schließt Dialoge?
- Space toggelt Switch/Checkbox?
ARIA
- Hat jedes Steuerelement einen accessible name?
- Liest der Screenreader Rolle und Status korrekt?
- Dynamische Änderungen mit passendem aria-live?
Visuell
- Fokus-Indikator klar sichtbar?
- Kontrast 4,5:1 bzw. 3:1?
- Information nicht nur über Farbe (Icons/Text dazu)?
Test-Tools
- Tastatur: Maus weg, Flow nur mit Tastatur
- Screenreader: VoiceOver (Mac) oder NVDA (Windows)
- Automatisiert: axe DevTools Browser-Erweiterung
Fazit
shadcn/ui gibt dir Code-Freiheit – mit Grenzen.
Die Grenze ist Radix’ Barrierefreiheits-Verhalten. Styling, Layout, Klassen – ok. Unterbau kaputt machen – nicht ok. button durch div, props vergessen – Tastatur-Nutzer leiden.
Merken:
- asChild: Kind fokussierbar, spread props + forward ref
- Fokus: Bei eigenem Dialog-Inhalt prüfen, wohin der Fokus geht
- ARIA: Radix setzt role – du lieferst label
Beim nächsten Umbau: erst Tastatur durch den Flow. Problem gefunden – fixen, nicht auf QA warten.
Barrierefreiheit ist keine Extra-Funktion, sondern Basis. shadcn/ui und Radix erledigen das Schwerste – übrig bleibt, die gute Arbeit nicht zu zerstören.
FAQ
Wie hängen shadcn/ui und Radix UI zusammen?
Wie nutzt man asChild, ohne Barrierefreiheit zu zerstören?
Worauf achten beim Fokus in eigenen Dialog-Inhalten?
Wie testet man, ob Barrierefreiheit funktioniert?
Radix setzt aria-Attribute automatisch – was bleibt für mich?
7 Min. Lesezeit · Veröffentlicht am: 30. März 2026 · Aktualisiert am: 13. Juli 2026
Tailwind & shadcn/ui in der Praxis
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
shadcn/ui Kompositionsmuster: Best Practices für mehrere Komponenten
Lernen Sie die Best Practices für shadcn/ui-Kompositionsmuster – Dialog+Form, DataTable+DropdownMenu und mehr. Mit Context-Muster, State-Management und Performance-Optimierung.
Teil 8 von 14
Nächster
Dialog, Sheet, Popover: Barrierefreiheit und Fokusmanagement bei Overlay-Komponenten
Tiefgehende Analyse der Barrierefreiheit und des Fokusmanagements bei shadcn/ui Dialog, Sheet und Popover – WCAG-Standards, ARIA-Attribute, Tastaturnavigation, Fokusfallen und vollständige Codebeispiele
Teil 10 von 14
Ähnliche Beiträge
Tailwind v4 + Vite: 5-Minuten-Setup-Vorlage und Verzeichnisstruktur

Tailwind v4 + Vite: 5-Minuten-Setup-Vorlage und Verzeichnisstruktur
Tailwind CSS v4 – Neue Features: Performance, Konfiguration und Migrationsleitfaden


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen