- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
590 lines
18 KiB
Markdown
590 lines
18 KiB
Markdown
# 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/)
|