Barrierefreie React UI mit Claude Code und Radix UI
Dialog, Dropdown und Tabs mit Claude Code und Radix UI umsetzen: Installation, React-Code, Styling, typische Fehler und Checkliste.
Ein Modal, ein Dropdown-Menü oder Tabs in React selbst zu bauen, wirkt zunächst harmlos. Die schwierigen Teile liegen aber nicht im Border Radius. Es geht um Tastaturbedienung, Fokusführung, Screenreader-Namen, Escape-Verhalten und mobile Breiten. Eine Komponente kann mit der Maus fertig aussehen und trotzdem für Tastaturnutzer unbrauchbar sein.
Radix UI ist für solche Fälle nützlich, weil es ungestylte Primitives liefert. Das Design bleibt bei dir: Farben, Abstände, Typografie und Animationen. Radix übernimmt dagegen viel vom Verhalten für Dialog, Dropdown Menu und Tabs. Genau diese Trennung macht Claude Code zuverlässiger.
Statt Claude Code zu bitten, “ein schönes Modal” zu erfinden, solltest du formulieren: “Nutze Radix UI, erhalte die Accessibility-Eigenschaften und passe das Styling an das bestehende Projekt an.” Für eine höhere Abstraktion auf Radix-Basis lies auch den Claude Code shadcn/ui Guide. Für eine breitere Accessibility-Prüfung passt der Claude Code Accessibility Guide.
Warum Radix UI zu Claude Code passt
Die offizielle Dokumentation beschreibt Radix Primitives als Low-Level-Komponentenbibliothek mit Fokus auf Accessibility, Anpassbarkeit und Developer Experience. Radix bringt also nicht dein fertiges Design mit, sondern die Verhaltensschicht: Rollen, Fokusmanagement, Tastaturinteraktionen und Komponentenstruktur.
Radix Dialog unterstützt etwa modale und nichtmodale Varianten, hält den Fokus im Modal, schließt per Escape und nutzt Title und Description für Screenreader-Ankündigungen. Radix Tabs folgt dem WAI-ARIA Tabs Pattern und behandelt Pfeiltasten, Home und End. Dropdown Menu liefert Labels, Separatoren, Radio Items und Untermenüs.
Claude Code arbeitet in einer Schleife aus Kontext sammeln, ändern und prüfen. Wenn es Dialog-Verhalten aus einfachen div-Elementen erzeugen soll, wird der Diff lang und schwer zu bewerten. Mit Radix UI kann Claude Code sich auf projektspezifische Arbeit konzentrieren: React State, API-Aufrufe, CSS-Klassen, Analytics und Tests.
flowchart LR
A["UI-Anforderung an Claude Code"] --> B["Radix UI für Verhalten nutzen"]
B --> C["React State und Produktlogik verbinden"]
C --> D["CSS oder Design Tokens anwenden"]
D --> E["Tastatur, Screenreader und Mobile prüfen"]
Installation
Radix dokumentiert inzwischen auch das gemeinsame Paket radix-ui. Viele React-Projekte verwenden aber weiterhin einzelne Pakete. Die Beispiele hier nutzen Einzelpakete, weil im package.json sofort sichtbar ist, welche Primitives gebraucht werden.
npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-tabs
Mit pnpm:
pnpm add @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-tabs
Wenn Claude Code noch nicht installiert ist, prüfe zuerst die offizielle Getting-Started-Dokumentation. Der npm-Weg ist:
npm install -g @anthropic-ai/claude-code
In Teams ist die Installation nur ein Teil der Arbeit. Klärt auch Updates, Berechtigungen, erlaubte Commands, Review-Regeln und welche Dateien Claude Code anfassen darf.
Prompt für Claude Code
Ein guter Prompt beschreibt nicht nur den Look, sondern das Interaktionsversprechen.
claude "Füge dem bestehenden React + TypeScript Screen einen Confirmation Dialog, ein User Dropdown Menu und Settings Tabs hinzu.
Anforderungen:
- @radix-ui/react-dialog, @radix-ui/react-dropdown-menu und @radix-ui/react-tabs verwenden
- Dialog.Title und Dialog.Description beibehalten
- Icon-only Close Buttons brauchen aria-label
- sichtbare Focus Styles nicht entfernen, :focus-visible verwenden
- vorhandene Design Tokens bevorzugen
- danach Keyboard-, Mobile-, Typecheck- und Accessibility-Checks auflisten"
Prüfe danach den Diff. Achte darauf, dass asChild keine verschachtelten Buttons erzeugt, Dialog.Title nicht aus optischen Gründen entfernt wurde und CSS den Fokus nicht unsichtbar macht.
Kopierbares React-Beispiel
Die folgende Datei enthält einen Bestätigungsdialog, ein User-Menü und Settings Tabs. Sie funktioniert in Vite, einer React SPA oder als Next.js Client Component. Im Next.js App Router kommt oben "use client"; dazu.
import * as React from "react";
import * as Dialog from "@radix-ui/react-dialog";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as Tabs from "@radix-ui/react-tabs";
import "./radix-accessible-demo.css";
type User = { name: string; email: string };
type ConfirmDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description: string;
confirmLabel?: string;
danger?: boolean;
onConfirm: () => Promise<void> | void;
};
export function ConfirmDialog({
open,
onOpenChange,
title,
description,
confirmLabel = "Confirm",
danger = false,
onConfirm,
}: ConfirmDialogProps) {
const [pending, setPending] = React.useState(false);
async function handleConfirm() {
setPending(true);
try {
await onConfirm();
onOpenChange(false);
} finally {
setPending(false);
}
}
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="radix-overlay" />
<Dialog.Content className="radix-dialog">
<Dialog.Title className="radix-dialog-title">{title}</Dialog.Title>
<Dialog.Description className="radix-dialog-description">
{description}
</Dialog.Description>
<div className="button-row">
<Dialog.Close asChild>
<button type="button" className="button secondary">Cancel</button>
</Dialog.Close>
<button
type="button"
className={`button ${danger ? "danger" : "primary"}`}
disabled={pending}
onClick={handleConfirm}
>
{pending ? "Working..." : confirmLabel}
</button>
</div>
<Dialog.Close asChild>
<button type="button" className="icon-button" aria-label="Close dialog">
x
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
export function UserMenu({
user,
onOpenProfile,
onOpenBilling,
onSignOut,
}: {
user: User;
onOpenProfile: () => void;
onOpenBilling: () => void;
onSignOut: () => void;
}) {
const [theme, setTheme] = React.useState<"light" | "dark" | "system">("system");
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button type="button" className="user-trigger" aria-label={`${user.name} menu`}>
<span className="avatar" aria-hidden="true">{user.name.slice(0, 1)}</span>
<span>{user.name}</span>
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="dropdown-content" align="end" sideOffset={8}>
<DropdownMenu.Label className="dropdown-label">{user.email}</DropdownMenu.Label>
<DropdownMenu.Separator className="dropdown-separator" />
<DropdownMenu.Item className="dropdown-item" onSelect={() => onOpenProfile()}>
Profile
</DropdownMenu.Item>
<DropdownMenu.Item className="dropdown-item" onSelect={() => onOpenBilling()}>
Billing
</DropdownMenu.Item>
<DropdownMenu.Separator className="dropdown-separator" />
<DropdownMenu.RadioGroup
value={theme}
onValueChange={(value) => setTheme(value as "light" | "dark" | "system")}
>
<DropdownMenu.RadioItem className="dropdown-item" value="light">Light</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem className="dropdown-item" value="dark">Dark</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem className="dropdown-item" value="system">System</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
<DropdownMenu.Separator className="dropdown-separator" />
<DropdownMenu.Item className="dropdown-item danger-text" onSelect={() => onSignOut()}>
Sign out
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}
export function SettingsTabs() {
return (
<Tabs.Root defaultValue="profile" className="tabs-root">
<Tabs.List className="tabs-list" aria-label="Account settings">
<Tabs.Trigger className="tabs-trigger" value="profile">Profile</Tabs.Trigger>
<Tabs.Trigger className="tabs-trigger" value="security">Security</Tabs.Trigger>
<Tabs.Trigger className="tabs-trigger" value="notifications">Notifications</Tabs.Trigger>
</Tabs.List>
<Tabs.Content className="tabs-content" value="profile">
<label className="field">
<span>Display name</span>
<input defaultValue="Masa" />
</label>
</Tabs.Content>
<Tabs.Content className="tabs-content" value="security">
<p>Require two-factor authentication before changing billing settings.</p>
<button type="button" className="button secondary">Review security</button>
</Tabs.Content>
<Tabs.Content className="tabs-content" value="notifications">
<label className="check-row">
<input type="checkbox" defaultChecked />
<span>Email me when a project is exported.</span>
</label>
</Tabs.Content>
</Tabs.Root>
);
}
Styling-Hinweise
Radix UI ist ungestylt. Du kannst Tailwind, CSS Modules, klassisches CSS oder Design Tokens nutzen. Wichtig sind sichtbarer Fokus, responsive Dialog-Breite und klare aktive Zustände.
.radix-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.55);
}
.radix-dialog {
position: fixed;
left: 50%;
top: 50%;
width: min(calc(100vw - 32px), 480px);
max-height: calc(100vh - 32px);
overflow: auto;
transform: translate(-50%, -50%);
border-radius: 8px;
background: white;
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.28);
padding: 24px;
}
.button-row {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
}
.dropdown-content {
min-width: 220px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: white;
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.18);
padding: 6px;
}
.dropdown-item {
border-radius: 6px;
cursor: pointer;
outline: none;
padding: 8px 10px;
}
.dropdown-item[data-highlighted] {
background: #eff6ff;
color: #1d4ed8;
}
.tabs-list {
display: flex;
border-bottom: 1px solid #e2e8f0;
gap: 4px;
}
.tabs-trigger {
border: 0;
border-bottom: 2px solid transparent;
background: transparent;
cursor: pointer;
padding: 10px 12px;
}
.tabs-trigger[data-state="active"] {
border-color: #2563eb;
color: #1d4ed8;
font-weight: 700;
}
:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 2px;
}
Drei Einsatzfälle
| Einsatzfall | Vorteil von Radix UI | Aufgabe für Claude Code |
|---|---|---|
| Lösch- oder Kündigungsdialog | Fokus, Titel und Beschreibung bleiben zuverlässig | API, Pending-State, Fehler, Tests |
| Account-Menü | Tastaturnavigation, Separatoren, Radio Items | Nutzerdaten, Logout, Billing, Analytics |
| Settings Tabs | Beziehung zwischen tablist, tab und panel bleibt stabil | Formulare, Speichern, URL-Sync, Dirty-State |
Der erste Fall ist ein SaaS-Adminbereich. Projekt löschen, Abo kündigen oder Rechte ändern muss eindeutig sein. Radix Dialog liefert das Verhalten, Claude Code verbindet API und Produkttext.
Der zweite Fall ist eine Kurs-, Medien- oder Membership-Seite. Im Account-Menü liegen Profil, Downloads, Kaufhistorie und Logout. Ein reines Klickmenü lässt Tastaturnutzer schnell hängen.
Der dritte Fall ist eine Einstellungsseite. Tabs trennen Profil, Sicherheit und Benachrichtigungen, aber die Speichergrenzen müssen klar bleiben. Diese Bedingung gehört direkt in den Prompt.
Typische Fehler
Entferne Dialog.Title nicht aus Designgründen. Wenn der Titel nicht sichtbar sein soll, braucht er trotzdem eine Screenreader-Variante. Die MDN-Dokumentation zum dialog role betont Name und Fokusmanagement.
Verstecke den Fokus nicht. Ein markenkonformer :focus-visible Stil ist gut; unsichtbarer Fokus ist ein Fehler.
Prüfe asChild. Ein button in einem button ist ungültiges HTML und kann unzuverlässig reagieren.
Staple Modals nicht leichtfertig. Das WAI-ARIA Modal Dialog Pattern erwartet, dass Fokus in den Dialog geht und beim Schließen zum Auslöser zurückkehrt.
Teste Mobile. Ein fixer 600px Dialog, ein rechts abgeschnittenes Menü oder lange Tab-Labels können Conversion und Bedienbarkeit ruinieren.
Offizielle Links und Checks
- Radix Primitives Introduction
- Radix Dialog docs
- Radix Dropdown Menu docs
- Radix Tabs docs
- WAI-ARIA Modal Dialog Pattern
- MDN dialog role
- Claude Code getting started
Vor dem Veröffentlichen: Dialog nur mit Tastatur öffnen und schließen, Escape prüfen, Fokus-Rückkehr prüfen, Dropdown per Pfeiltasten bedienen, Tabs per Pfeiltasten wechseln und Mobile Overflow prüfen.
Monetarisierungs-CTA
Dieses Thema eignet sich gut für eine Umsetzungsberatung. Leser wollen nicht nur wissen, was Radix UI ist, sondern wie sie es sicher in ihr bestehendes Repository einbauen.
ClaudeCodeLab unterstützt Teams über Claude Code Training und Beratung: CLAUDE.md, Komponentenregeln, Accessibility-Checks, Review-Prompts und Verifikationsskripte. Solo-Entwickler können mit Checklisten und Templates starten; Teams profitieren eher von einer Review-Session an einem echten Screen.
Getestetes Ergebnis
In Masas React-Testscreen wurde ein handgeschriebenes Modal durch Radix Dialog ersetzt, außerdem wanderten Menü und Tabs zu Radix. Der Code wurde etwas länger, aber die Review-Kriterien wurden klarer. Claude Code lieferte bessere Hinweise, wenn es gezielt Fokus-Rückkehr, Screenreader-Namen, Mobile-Breite und Tastaturbedienung prüfen sollte. Radix UI ersetzt Accessibility-Denken nicht; es gibt Claude Code eine sicherere Grundlage.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Workflow von Obsidian zu CLAUDE.md
Obsidian-Arbeitsnotizen in CLAUDE.md-Betriebsnotizen verwandeln und Kontext nicht ständig neu erklären.
Claude Code Revenue CTA Routing: Artikel zu PDF, Gumroad und Beratung führen
Ein Claude-Code-Ablauf, der Leser nach Absicht zu Gratis-PDF, Gumroad oder Beratung führt.
Claude-Code-Team-Handoff-Regeln: Belege, Berechtigungen, Rollback und Umsatzpfade
Ein praktisches Claude-Code-Handoff für Review-Belege, Berechtigungen, Rollback, Gratis-PDF, Gumroad und Beratung.