Files
Portfolio/blog-posts/40-radix-ui-components.md
damjan_savicandClaude Opus 4.5 43484c5023 Add blog posts, cleanup unused files, update components
- 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>
2026-01-25 11:42:11 +01:00

590 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/)