Modales & Dialogues
Règles
- Utiliser Radix Dialog ou shadcn
<Dialog> — accessible, piégeage du focus, verrouillage du défilement par défaut
- Dialogues de confirmation pour les actions destructrices : « Supprimer ce projet ? » avec les boutons Annuler (secondaire) et Supprimer (destructeur)
- Panneaux / tiroirs pour mobile :
<Sheet> de shadcn — glisse depuis le bas ou le côté, meilleure cible tactile que les modales centrées
- Piège de focus : Radix le gère automatiquement — le premier élément focusable reçoit le focus à l'ouverture, le focus revient à la fermeture
- Verrouillage du défilement : empêcher le défilement du corps lorsque la modale est ouverte — Radix le gère avec
<Dialog.Overlay>
- Déclencheurs de fermeture : touche Échap, clic en dehors de l'overlay, bouton de fermeture explicite — tous fournis par Radix
- État contrôlé : propriétés
open + onOpenChange — ne pas se fier à l'état interne pour les modales déclenchées par des événements externes
- Formulaires dans les dialogues : soumettre avec Entrée, fermer en cas de succès, afficher les erreurs en ligne — ne pas fermer en cas d'échec de validation
- Mobile : utiliser
<Sheet> (tiroir inférieur) plutôt qu'un <Dialog> centré pour les actions sur petits écrans
Patterns
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">Supprimer le projet</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Supprimer ce projet ?</AlertDialogTitle>
<AlertDialogDescription>
Cette action supprimera définitivement le projet et toutes ses données. Elle est irréversible.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Annuler</AlertDialogCancel>
<AlertDialogAction
onClick={async () => { setLoading(true); await onDelete(); setLoading(false); }}
disabled={loading}
>
{loading ? "Suppression..." : "Supprimer"}
</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>Actions</SheetTitle></SheetHeader>
<nav className="flex flex-col gap-2 py-4">
<Button variant="ghost">Modifier</Button>
<Button variant="ghost">Partager</Button>
<Button variant="destructive">Supprimer</Button>
</nav>
</SheetContent>
</Sheet>
);
}
À Éviter
- Construire des modales personnalisées avec
position: fixed et gestion manuelle du focus — utiliser Radix/shadcn Dialog
- Oublier la touche Échap et le clic en dehors pour fermer — Radix gère les deux, ne pas les surcharger
- Modales centrées sur mobile pour les menus d'action — utiliser un Sheet/Drawer inférieur pour une meilleure accessibilité au pouce
- Fermer le dialogue en cas d'échec de validation de formulaire — le laisser ouvert, afficher les erreurs en ligne, laisser l'utilisateur corriger et resoumettre
- Dialogues imbriqués sans contexte d'empilement approprié — si nécessaire, utiliser le support imbriqué de Radix avec un
z-index correct