Skip to main content

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
string
default:"md"
Size of the dialog: "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "6xl" | "7xl" | "full"
className
string
Additional CSS classes to apply

DialogTrigger

isOpen
boolean
Controlled open state
defaultOpen
boolean
Default open state (uncontrolled)
onOpenChange
(isOpen: boolean) => void
Callback when open state changes

DialogOverlay

isDismissable
boolean
default:true
Whether clicking outside closes the dialog
isKeyboardDismissDisabled
boolean
default:false
Whether pressing Escape closes the dialog
className
string
Additional CSS classes to apply

DialogHeader

className
string
Additional CSS classes to apply

DialogBody

className
string
Additional CSS classes to apply

DialogFooter

className
string
Additional CSS classes to apply

DialogTitle

className
string
Additional CSS classes to apply

DialogDescription

className
string
Additional CSS classes to apply

DialogClose

className
string
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>

Information Dialog

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>

Form Dialog

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="[email protected]"
            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

  1. Clear purpose: Use dialogs for focused tasks requiring immediate attention
  2. Concise content: Keep dialog content brief and scannable
  3. Obvious actions: Make primary and secondary actions clear
  4. Keyboard support: Ensure all interactions work via keyboard
  5. Appropriate size: Choose size based on content needs
  6. Avoid nested dialogs: Don’t open dialogs from within dialogs
  7. Semantic variants: Use appropriate semantic styling (success, danger, info)
  8. 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.

Build docs developers (and LLMs) love