Overview
The Dialog component provides an accessible modal overlay for displaying content that requires user attention or interaction. Built on React Aria Components with focus trapping, keyboard navigation, and semantic design tokens.
Installation
import {
Dialog,
DialogTrigger,
DialogOverlay,
DialogHeader,
DialogBody,
DialogFooter,
DialogTitle,
DialogDescription,
DialogClose,
} from "@/components/ui/Dialog";
Anatomy
The Dialog is composed of multiple sub-components:
- DialogTrigger: Wraps trigger and dialog (uses React Aria’s DialogTrigger)
- DialogOverlay: Backdrop and modal container (uses React Aria’s ModalOverlay + Modal)
- Dialog: Main dialog content container (uses React Aria’s Dialog)
- DialogHeader: Header section with title and close button
- DialogBody: Main content area
- DialogFooter: Footer for action buttons
- DialogTitle: Dialog heading (uses React Aria’s Heading)
- DialogDescription: Supporting description text
- DialogClose: Close button (uses React Aria’s Button with slot=“close”)
Basic Usage
<DialogTrigger>
<Button variant="primary">Open Dialog</Button>
<DialogOverlay>
<Dialog>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
<DialogClose>
<X size={16} />
</DialogClose>
</DialogHeader>
<DialogBody>
<DialogDescription>
This is a basic dialog example. You can place any content here.
</DialogDescription>
</DialogBody>
<DialogFooter>
<Button variant="secondary" slot="close">
Cancel
</Button>
<Button variant="primary">Confirm</Button>
</DialogFooter>
</Dialog>
</DialogOverlay>
</DialogTrigger>
Sizes
The Dialog supports multiple size variants:
{/* Small - 384px max width */}
<Dialog size="sm">
{/* Dialog content */}
</Dialog>
{/* Medium - 448px max width (default) */}
<Dialog size="md">
{/* Dialog content */}
</Dialog>
{/* Large - 512px max width */}
<Dialog size="lg">
{/* Dialog content */}
</Dialog>
{/* Extra Large - 576px max width */}
<Dialog size="xl">
{/* Dialog content */}
</Dialog>
{/* 2XL - 672px max width */}
<Dialog size="2xl">
{/* Dialog content */}
</Dialog>
{/* 3XL through 7XL also available */}
<Dialog size="3xl">{/* 768px */}</Dialog>
<Dialog size="4xl">{/* 896px */}</Dialog>
<Dialog size="5xl">{/* 1024px */}</Dialog>
<Dialog size="6xl">{/* 1152px */}</Dialog>
<Dialog size="7xl">{/* 1280px */}</Dialog>
{/* Full - Full viewport */}
<Dialog size="full">
{/* Dialog content */}
</Dialog>
Props
Dialog
Size of the dialog: "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "6xl" | "7xl" | "full"
Additional CSS classes to apply
DialogTrigger
Default open state (uncontrolled)
onOpenChange
(isOpen: boolean) => void
Callback when open state changes
DialogOverlay
Whether clicking outside closes the dialog
isKeyboardDismissDisabled
Whether pressing Escape closes the dialog
Additional CSS classes to apply
Additional CSS classes to apply
DialogBody
Additional CSS classes to apply
Additional CSS classes to apply
DialogTitle
Additional CSS classes to apply
DialogDescription
Additional CSS classes to apply
DialogClose
Additional CSS classes to apply
Semantic Dialog Types
Confirmation Dialog
For destructive or important actions:
import { AlertTriangle } from "lucide-react";
<DialogTrigger>
<Button variant="destructive">Delete Item</Button>
<DialogOverlay>
<Dialog size="sm">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<AlertTriangle size={20} className="[color:var(--status-danger)]" />
Confirm Deletion
</DialogTitle>
<DialogClose>
<X size={16} />
</DialogClose>
</DialogHeader>
<DialogBody>
<DialogDescription>
Are you sure you want to delete this item? This action cannot be
undone and will permanently remove the item from your account.
</DialogDescription>
</DialogBody>
<DialogFooter>
<Button variant="secondary" slot="close">
Cancel
</Button>
<Button variant="destructive">Delete</Button>
</DialogFooter>
</Dialog>
</DialogOverlay>
</DialogTrigger>
Success Dialog
For positive feedback:
import { CheckCircle } from "lucide-react";
<DialogTrigger>
<Button variant="primary">Complete Action</Button>
<DialogOverlay>
<Dialog size="sm">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<CheckCircle size={20} className="[color:var(--status-success)]" />
Success!
</DialogTitle>
<DialogClose>
<X size={16} />
</DialogClose>
</DialogHeader>
<DialogBody>
<DialogDescription>
Your action has been completed successfully. All changes have been
saved and will take effect immediately.
</DialogDescription>
</DialogBody>
<DialogFooter>
<Button variant="primary" slot="close">
Continue
</Button>
</DialogFooter>
</Dialog>
</DialogOverlay>
</DialogTrigger>
For displaying information:
import { Info } from "lucide-react";
<DialogTrigger>
<Button variant="secondary" leftIcon={<Info size={16} />}>
Show Information
</Button>
<DialogOverlay>
<Dialog size="md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Info size={20} className="[color:var(--interactive-primary)]" />
Information
</DialogTitle>
<DialogClose>
<X size={16} />
</DialogClose>
</DialogHeader>
<DialogBody>
<DialogDescription>
This is an informational dialog that provides important details.
</DialogDescription>
<div className="mt-4 p-4 rounded-lg [background-color:var(--status-success-bg)] [border:1px_solid_var(--status-success)]">
<p className="text-sm [color:var(--status-success-text)]">
💡 <strong>Tip:</strong> You can use dialogs to provide
contextual help and guidance.
</p>
</div>
</DialogBody>
<DialogFooter>
<Button variant="primary" slot="close">
Understood
</Button>
</DialogFooter>
</Dialog>
</DialogOverlay>
</DialogTrigger>
Dialog containing a form:
import { User, Mail } from "lucide-react";
<DialogTrigger>
<Button variant="primary" leftIcon={<User size={16} />}>
Edit Profile
</Button>
<DialogOverlay>
<Dialog size="lg">
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
<DialogClose>
<X size={16} />
</DialogClose>
</DialogHeader>
<DialogBody>
<form className="space-y-4">
<Input
label="Name"
placeholder="Enter your name"
leftIcon={<User size={16} />}
defaultValue="John Doe"
isRequired
/>
<Input
label="Email"
type="email"
placeholder="Enter your email"
leftIcon={<Mail size={16} />}
defaultValue="john@example.com"
isRequired
/>
<div>
<label className="block text-sm font-medium mb-1 [color:var(--text-primary)]">
Bio
</label>
<textarea
rows={3}
placeholder="Tell us about yourself"
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
</form>
</DialogBody>
<DialogFooter>
<Button variant="secondary" slot="close">
Cancel
</Button>
<Button variant="primary">Save Changes</Button>
</DialogFooter>
</Dialog>
</DialogOverlay>
</DialogTrigger>
Scrollable Content
For lengthy content:
import { FileText } from "lucide-react";
<DialogTrigger>
<Button variant="primary" leftIcon={<FileText size={16} />}>
View Terms
</Button>
<DialogOverlay>
<Dialog size="2xl">
<DialogHeader>
<DialogTitle>Terms and Conditions</DialogTitle>
<DialogClose>
<X size={16} />
</DialogClose>
</DialogHeader>
<DialogBody className="max-h-96 overflow-y-auto">
<div className="prose prose-sm [color:var(--text-primary)]">
<p>Welcome to our Terms and Conditions...</p>
<h3>1. Acceptance of Terms</h3>
<p>By accessing and using this service...</p>
{/* More content */}
</div>
</DialogBody>
<DialogFooter>
<Button variant="secondary" slot="close">
Decline
</Button>
<Button variant="primary">Accept</Button>
</DialogFooter>
</Dialog>
</DialogOverlay>
</DialogTrigger>
Controlled Dialog
Manage dialog state externally:
const [isOpen, setIsOpen] = useState(false);
<DialogTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
<Button onClick={() => setIsOpen(true)}>Open Dialog</Button>
<DialogOverlay>
<Dialog>
{/* Dialog content */}
</Dialog>
</DialogOverlay>
</DialogTrigger>
Non-Dismissable Dialog
Prevent closing by clicking outside or pressing Escape:
<DialogTrigger>
<Button>Critical Action</Button>
<DialogOverlay isDismissable={false} isKeyboardDismissDisabled={true}>
<Dialog>
<DialogHeader>
<DialogTitle>Important</DialogTitle>
{/* No close button */}
</DialogHeader>
<DialogBody>
<DialogDescription>
You must make a choice to proceed. This dialog cannot be dismissed.
</DialogDescription>
</DialogBody>
<DialogFooter>
<Button variant="secondary" slot="close">
Cancel
</Button>
<Button variant="primary" slot="close">
Confirm
</Button>
</DialogFooter>
</Dialog>
</DialogOverlay>
</DialogTrigger>
React Aria Components Integration
The Dialog uses React Aria Components for:
Focus Management
- Auto-focus on dialog open
- Focus trapping within dialog
- Focus restoration on close
Accessibility
- Proper ARIA roles and attributes
- Keyboard navigation support
- Screen reader announcements
Features
- Backdrop click handling
- Escape key handling
- Multiple dialog stacking
- Smooth animations
Accessibility Features
Keyboard Navigation
- Escape: Close dialog (if dismissable)
- Tab: Move focus between interactive elements
- Shift + Tab: Move focus backward
ARIA Attributes
role="dialog" on Dialog
aria-modal="true" indicates modal behavior
aria-labelledby associates title with dialog
aria-describedby associates description with dialog
Focus Management
- Focus moves to dialog on open
- Focus trapped within dialog
- Focus restored to trigger on close
- First focusable element receives focus
Screen Reader Support
- Dialog opening is announced
- Title and description are read
- Close actions are communicated
Design Tokens
The Dialog uses semantic design tokens:
--bg-overlay: Backdrop color with opacity
--bg-primary: Dialog background
--border-primary: Dialog border
--text-primary/secondary: Text colors
--border-focus: Focus ring color
--font-family-primary: Typography
Best Practices
- Clear purpose: Use dialogs for focused tasks requiring immediate attention
- Concise content: Keep dialog content brief and scannable
- Obvious actions: Make primary and secondary actions clear
- Keyboard support: Ensure all interactions work via keyboard
- Appropriate size: Choose size based on content needs
- Avoid nested dialogs: Don’t open dialogs from within dialogs
- Semantic variants: Use appropriate semantic styling (success, danger, info)
- Cancel option: Always provide a way to cancel or close
Animation
Dialogs include smooth enter/exit animations:
- Overlay fades in/out
- Dialog content scales and fades
- Animations use React Aria’s data attributes
- Custom timing via CSS variables
<DialogOverlay className="[--modal-animation-duration-in:500ms]">
<Dialog>
{/* Slower opening animation */}
</Dialog>
</DialogOverlay>
Dark Mode
Dialogs automatically adapt to dark mode using semantic tokens:
<DialogTrigger>
<Button>Open Dialog</Button>
<DialogOverlay>
<Dialog>
{/* Automatically adapts to theme */}
</Dialog>
</DialogOverlay>
</DialogTrigger>
All colors, backgrounds, and borders adjust based on the active theme.