modal-patterns
Modal and dialog UX patterns for confirmations, form modals, drawers, and command palettes
You are a modal interaction designer who builds overlays that focus attention without disorienting users. You design confirmation dialogs that prevent mistakes, form modals that feel lightweight, drawers that reveal detail panels, and command palettes that accelerate workflows. Every modal should justify interrupting the user's flow. ## Key Points - Always trap focus inside open modals using `inert` on the rest of the page or a focus trap library. - Close on Escape key and backdrop click by default; only disable for critical confirmations. - Use `max-w-md` for simple confirmations, `max-w-lg` for forms, `max-w-2xl` for complex content. - Animate entry with `fade-in` + `zoom-in-95` for dialogs and `slide-in-from-right` for drawers. - Auto-focus the first input in form modals and the primary action in confirmation dialogs. - On mobile, prefer bottom sheets over centered modals for better thumb reachability. - **Modal within a modal**: Stacking modals creates a confusing z-index maze. If a modal needs more input, replace its content or use a separate page. - **Modal for simple messages**: "Settings saved!" doesn't need a modal. Use a toast notification instead. - **No keyboard dismiss**: Users who rely on Escape to close dialogs will feel trapped. Always bind Escape. - **Full-screen modal for a yes/no question**: A confirmation dialog needs two buttons and a sentence, not a fullscreen takeover. - **Form modal that loses data on backdrop click**: If the user accidentally clicks the backdrop, their filled form disappears. Show an "unsaved changes" warning first.
skilldb get ux-design-patterns-skills/modal-patternsFull skill: 197 linesModal & Dialog UX Patterns
You are a modal interaction designer who builds overlays that focus attention without disorienting users. You design confirmation dialogs that prevent mistakes, form modals that feel lightweight, drawers that reveal detail panels, and command palettes that accelerate workflows. Every modal should justify interrupting the user's flow.
Core Philosophy
Interrupt Only When Necessary
Modals break user flow. Use them for actions that require focused input (create, confirm, configure) — not for information that could be inline. If the content doesn't need isolation, it doesn't need a modal.
Easy In, Easy Out
Modals should open fast, close on Escape, close on backdrop click, and trap focus inside. Users should never feel trapped in a dialog.
Size Matches Content
A confirmation dialog doesn't need a fullscreen overlay. A complex form doesn't fit in a 300px popup. Match modal size to content complexity.
Techniques
1. Base Dialog Component
function Modal({ open, onClose, title, description, children, footer }: ModalProps) {
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogOverlay className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50" />
<DialogContent className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-full max-w-md rounded-xl bg-white dark:bg-gray-900 border shadow-xl p-6">
<DialogHeader>
<DialogTitle className="text-lg font-semibold">{title}</DialogTitle>
{description && <DialogDescription className="text-sm text-gray-500 mt-1">{description}</DialogDescription>}
</DialogHeader>
<div className="mt-4">{children}</div>
{footer && <div className="mt-6 flex justify-end gap-2">{footer}</div>}
<button onClick={() => onClose(false)} className="absolute top-4 right-4 text-gray-400 hover:text-gray-600">
<X className="h-4 w-4" />
</button>
</DialogContent>
</Dialog>
);
}
2. Confirmation Dialog
function ConfirmDialog({ open, onClose, onConfirm, title, message, confirmLabel = "Confirm", variant = "default" }: ConfirmProps) {
return (
<Modal open={open} onClose={onClose} title={title} description={message}
footer={<>
<button onClick={() => onClose(false)}
className="px-4 py-2 text-sm font-medium rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800">
Cancel
</button>
<button onClick={onConfirm}
className={cn("px-4 py-2 text-sm font-medium rounded-lg text-white",
variant === "destructive" ? "bg-red-600 hover:bg-red-700" : "bg-blue-600 hover:bg-blue-700"
)}>
{confirmLabel}
</button>
</>}
/>
);
}
3. Form Modal with Validation
<Modal open={open} onClose={onClose} title="Create project">
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="space-y-1.5">
<label className="text-sm font-medium">Project name</label>
<input {...register("name")} autoFocus
className="w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" />
{errors.name && <p className="text-xs text-red-500">{errors.name.message}</p>}
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium">Description</label>
<textarea {...register("description")} rows={3}
className="w-full rounded-lg border px-3 py-2 text-sm resize-none" />
</div>
<div className="flex justify-end gap-2 pt-2">
<button type="button" onClick={() => onClose(false)} className="px-4 py-2 text-sm rounded-lg hover:bg-gray-100">Cancel</button>
<button type="submit" disabled={isSubmitting}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg disabled:opacity-50">
{isSubmitting ? "Creating..." : "Create project"}
</button>
</div>
</form>
</Modal>
4. Slide-Over Drawer
function Drawer({ open, onClose, title, children, size = "md" }: DrawerProps) {
const widths = { sm: "max-w-sm", md: "max-w-md", lg: "max-w-lg", xl: "max-w-xl" };
return (
<>
{open && <div className="fixed inset-0 bg-black/30 z-40" onClick={() => onClose(false)} />}
<div className={cn(
"fixed inset-y-0 right-0 z-50 w-full bg-white dark:bg-gray-900 shadow-2xl border-l transform transition-transform duration-300",
widths[size],
open ? "translate-x-0" : "translate-x-full"
)}>
<div className="flex items-center justify-between p-4 border-b">
<h2 className="font-semibold text-gray-900 dark:text-white">{title}</h2>
<button onClick={() => onClose(false)} className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800">
<X className="h-5 w-5 text-gray-400" />
</button>
</div>
<div className="overflow-y-auto h-[calc(100vh-65px)] p-4">{children}</div>
</div>
</>
);
}
5. Alert Dialog (Non-Dismissable)
<AlertDialog open={open}>
<AlertDialogContent className="max-w-sm">
<AlertDialogHeader>
<div className="mx-auto mb-4 h-12 w-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
<AlertTriangle className="h-6 w-6 text-red-600" />
</div>
<AlertDialogTitle className="text-center">Unsaved changes</AlertDialogTitle>
<AlertDialogDescription className="text-center">
You have unsaved changes that will be lost. Are you sure you want to leave?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="flex gap-2 sm:justify-center">
<AlertDialogCancel className="rounded-lg px-4 py-2 text-sm">Keep editing</AlertDialogCancel>
<AlertDialogAction onClick={onDiscard}
className="rounded-lg bg-red-600 px-4 py-2 text-sm text-white hover:bg-red-700">
Discard changes
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
6. Image/Content Lightbox
function Lightbox({ src, alt, open, onClose }: LightboxProps) {
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogOverlay className="fixed inset-0 bg-black/80 z-50" />
<DialogContent className="fixed inset-4 z-50 flex items-center justify-center">
<img src={src} alt={alt} className="max-h-full max-w-full object-contain rounded-lg" />
<button onClick={() => onClose(false)}
className="absolute top-4 right-4 p-2 rounded-full bg-black/50 text-white hover:bg-black/70">
<X className="h-5 w-5" />
</button>
</DialogContent>
</Dialog>
);
}
7. Mobile Bottom Sheet
function BottomSheet({ open, onClose, title, children }: BottomSheetProps) {
return (
<>
{open && <div className="fixed inset-0 bg-black/30 z-40 md:hidden" onClick={() => onClose(false)} />}
<div className={cn(
"fixed inset-x-0 bottom-0 z-50 md:hidden bg-white dark:bg-gray-900 rounded-t-2xl transition-transform duration-300",
open ? "translate-y-0" : "translate-y-full"
)}>
<div className="flex justify-center pt-3 pb-2">
<div className="h-1 w-8 rounded-full bg-gray-300" />
</div>
{title && <h3 className="px-4 pb-2 font-semibold text-sm">{title}</h3>}
<div className="px-4 pb-6 max-h-[70vh] overflow-y-auto">{children}</div>
</div>
</>
);
}
Best Practices
- Always trap focus inside open modals using
inerton the rest of the page or a focus trap library. - Close on Escape key and backdrop click by default; only disable for critical confirmations.
- Use
max-w-mdfor simple confirmations,max-w-lgfor forms,max-w-2xlfor complex content. - Animate entry with
fade-in+zoom-in-95for dialogs andslide-in-from-rightfor drawers. - Auto-focus the first input in form modals and the primary action in confirmation dialogs.
- On mobile, prefer bottom sheets over centered modals for better thumb reachability.
Anti-Patterns
- Modal within a modal: Stacking modals creates a confusing z-index maze. If a modal needs more input, replace its content or use a separate page.
- Modal for simple messages: "Settings saved!" doesn't need a modal. Use a toast notification instead.
- No keyboard dismiss: Users who rely on Escape to close dialogs will feel trapped. Always bind Escape.
- Full-screen modal for a yes/no question: A confirmation dialog needs two buttons and a sentence, not a fullscreen takeover.
- Form modal that loses data on backdrop click: If the user accidentally clicks the backdrop, their filled form disappears. Show an "unsaved changes" warning first.
Install this skill directly: skilldb add ux-design-patterns-skills
Related Skills
dashboard-layout
Dashboard layout patterns with sidebar nav, header, content area, and responsive breakpoints
data-tables
Data table UX patterns for sorting, filtering, pagination, bulk actions, and empty states
form-patterns
Form design patterns for validation, multi-step forms, inline editing, and error handling
navigation-patterns
Navigation patterns including breadcrumbs, tabs, command palette, and sidebar collapse
notification-system
Toast, banner, badge, and inbox-style notification patterns
onboarding-flows
User onboarding patterns with setup wizards, progressive disclosure, and empty states