Modals & Dialoge
Regeln
- Verwende Radix Dialog oder shadcn
<Dialog> – standardmäßig barrierefrei, mit Fokus-Falle und Scroll-Sperre
- Bestätigungsdialoge für destruktive Aktionen: „Dieses Projekt löschen?“ mit Abbrechen (sekundär) und Löschen (destruktiv)
- Sheets/Drawer für Mobilgeräte:
<Sheet> von shadcn – gleitet von unten oder der Seite herein, bietet bessere Touch-Ziele als zentrierte Modals
- Fokus-Falle: Radix übernimmt das automatisch – das erste fokussierbare Element erhält beim Öffnen den Fokus, beim Schließen wird der Fokus zurückgesetzt
- Scroll-Sperre: Verhindert Body-Scroll, wenn das Modal geöffnet ist – Radix erledigt dies mit
<Dialog.Overlay>
- Schließen-Auslöser: Escape-Taste, Klick außerhalb des Overlays, explizite Schließen-Schaltfläche – alles von Radix bereitgestellt
- Kontrollierter Zustand:
open + onOpenChange Props – verlasse dich nicht auf den internen Zustand für Modals, die durch externe Ereignisse ausgelöst werden
- Formulare in Dialogen: Absenden mit Enter, schließen bei Erfolg, Fehler inline anzeigen – nicht bei Validierungsfehlern schließen
- Mobilgeräte: Verwende
<Sheet> (unterer Drawer) anstelle eines zentrierten <Dialog> für Aktionen auf kleinen Bildschirmen
Muster
import {
AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
AlertDialogDescription, AlertDialogFooter, AlertDialogHeader,
AlertDialogTitle, AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
function DeleteButton({ onDelete }: { onDelete: () => Promise<void> }) {
const [loading, setLoading] = useState(false);
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">Projekt löschen</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Dieses Projekt löschen?</AlertDialogTitle>
<AlertDialogDescription>
Dies löscht das Projekt und alle zugehörigen Daten endgültig. Diese Aktion kann nicht rückgängig gemacht werden.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Abbrechen</AlertDialogCancel>
<AlertDialogAction
onClick={async () => { setLoading(true); await onDelete(); setLoading(false); }}
disabled={loading}
>
{loading ? "Löschen..." : "Löschen"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
function MobileMenu() {
return (
<Sheet>
<SheetTrigger asChild><Button variant="ghost" size="icon"><MenuIcon /></Button></SheetTrigger>
<SheetContent side="bottom">
<SheetHeader><SheetTitle>Aktionen</SheetTitle></SheetHeader>
<nav className="flex flex-col gap-2 py-4">
<Button variant="ghost">Bearbeiten</Button>
<Button variant="ghost">Teilen</Button>
<Button variant="destructive">Löschen</Button>
</nav>
</SheetContent>
</Sheet>
);
}
Vermeiden
- Eigene Modals mit
position: fixed und manuellem Fokus-Management zu bauen – verwende Radix/shadcn Dialog
- Fehlende Escape-Taste und Klick-außerhalb-zum-Schließen – Radix übernimmt beides, überschreibe es nicht
- Zentrierte Modals auf Mobilgeräten für Aktionsmenüs – verwende untere Sheets/Drawer für bessere Daumenreichweite
- Dialog bei Formularvalidierungsfehlern schließen – offen lassen, Inline-Fehler anzeigen, Benutzer korrigieren und erneut absenden lassen
- Verschachtelte Dialoge ohne korrekte Stapelkontext – falls nötig, Radix' Unterstützung für verschachtelte Dialoge mit passendem
z-index verwenden