Skip to content

Dialog

An overlay window that requires user interaction before returning to the main content.

  • Modal
  • Popup
  • Lightbox

Overview

The Dialog (also called a modal) is an overlay window that appears on top of the main content, requiring user interaction before they can return to the underlying page. It demands attention — and that's exactly why you should use it sparingly.

Dialogs interrupt the user's flow. Every time you open one, you're saying: "Stop what you're doing and deal with this first." That's sometimes necessary (confirming a destructive action, collecting required input), but it's frequently overused. If the content can live inline on the page, it should.

When to use a Dialog:

  • Confirming irreversible actions ("Delete this project? This cannot be undone.")
  • Collecting focused input that requires context isolation (a quick form, a settings change)
  • Displaying critical information that must be acknowledged before proceeding
  • Previewing content (image lightbox, video player)

When NOT to use a Dialog:

  • For information that isn't urgent — use an Alert or Toast instead
  • For complex, multi-step forms — use a dedicated page or a Stepper
  • For navigation — use a Drawer or Sidebar
  • For simple confirmations — consider an inline confirmation pattern

Enhance your dialog's visual treatment with our Shadow Generator for depth, the Glassmorphism Generator for frosted-glass backdrops, and the Transition Generator for smooth open/close animations.

Variants

Common Dialog Variants

VariantPurposeTypical Size
Alert DialogConfirms a user action, especially destructive ones. Has a clear cancel + confirm button pair.Small (400–480px wide)
Form DialogContains a short form (1–5 fields) for focused data entry.Medium (480–560px wide)
Full-screen DialogComplex content that needs maximum space (e.g., rich text editor, image cropper).Full viewport on mobile, max ~900px on desktop
Drawer-style DialogSlides in from the side rather than appearing centered. See Drawer.320–480px wide, full height
Nested DialogA dialog opened from within another dialog. Avoid if at all possible — stacking modals is a UX anti-pattern.Same as parent
Non-modal DialogDoes not block interaction with the page behind it. Think chat widgets or sticky panels.Variable

Size Variants

SizeMax WidthUse Case
Small400pxConfirmations, simple alerts
Medium560pxShort forms, settings panels
Large720pxComplex content, data previews
Full100vw (with padding)Mobile-first, image viewers

Use the Animation Generator to craft entrance/exit animations for your dialog.

Properties

Dialog Properties

PropertyTypeDefaultDescription
openbooleanfalseControls whether the dialog is visible
onClose() => void—Callback when the dialog is dismissed (backdrop click, Escape key, close button)
size'sm' | 'md' | 'lg' | 'full''md'Max-width of the dialog panel
closeOnBackdropClickbooleantrueWhether clicking the backdrop dismisses the dialog
closeOnEscapebooleantrueWhether pressing Escape dismisses the dialog
initialFocusRefObject<HTMLElement>—Element to focus when the dialog opens (defaults to first focusable element)
returnFocusbooleantrueReturn focus to the trigger element on close
role'dialog' | 'alertdialog''dialog'Use alertdialog for confirmations that require acknowledgment
aria-labelledbystring—ID of the dialog title element
aria-describedbystring—ID of the dialog description element
preventScrollbooleantruePrevents body scroll when dialog is open

Token Mappings

Design Token Mappings

Token CategoryToken ExampleDialog Usage
Color – Surface--color-surface-elevatedDialog panel background
Color – Backdrop--color-overlay (rgba(0,0,0,0.5))Semi-transparent backdrop. Try Glassmorphism Generator for frosted glass.
Border Radius--radius-xl (16px)Dialog corners. Preview with Border Radius Generator.
Shadow--shadow-2xlDialog elevation. Configure with Shadow Generator.
Spacing – Padding--space-6 (24px)Internal padding of the dialog body
Spacing – Gap--space-4 (16px)Gap between title, body, and footer
Typography--font-size-lg, --font-weight-semiboldDialog title styling
Transition--duration-normal (200ms)Open/close animation duration
Z-index--z-modal (50)Stacking above page content

For a comprehensive look at token architecture, read our Design Tokens Complete Guide.

States

Dialog States

StateVisual BehaviorImplementation
ClosedNot rendered or rendered with display: none. No DOM presence is ideal for performance.Remove from DOM or use hidden attribute
OpeningBackdrop fades in (opacity 0→1). Panel scales up or slides in.CSS transition or animation. Use Transition Generator for values.
OpenBackdrop visible. Panel centered (or positioned). Focus trapped inside. Body scroll locked.aria-modal="true", inert on background content
ClosingReverse of opening animation. Focus returns to trigger.Delay unmounting until animation completes
NestedSecond backdrop layers on top. Each dialog manages its own focus trap.Maintain a dialog stack — close inner before outer

Backdrop States

  • Default: Semi-transparent black (rgba(0,0,0,0.5))
  • Blur: Frosted-glass effect (backdrop-filter: blur(8px)). Creates depth but impacts performance on low-end devices.
  • Interactive: Clicking the backdrop closes the dialog. For alert dialogs, disable this — force users to make a choice.

Accessibility

WCAG Requirements

CriterionLevelRequirement
SC 1.3.1 Info and RelationshipsADialog must be marked with role="dialog" or use the native <dialog> element
SC 2.1.2 No Keyboard TrapAUsers must be able to close the dialog via keyboard (Escape key)
SC 2.4.3 Focus OrderAFocus must move into the dialog when it opens and return to the trigger when it closes
SC 4.1.2 Name, Role, ValueADialog must have an accessible name via aria-labelledby or aria-label

ARIA Attributes

  • role="dialog" — Standard dialog. Use the native <dialog> element when possible (it gives you a lot for free).
  • role="alertdialog" — For dialogs that require user acknowledgment (confirmations, warnings). Screen readers announce these more urgently.
  • aria-modal="true" — Tells assistive tech that content behind the dialog is inert. Essential.
  • aria-labelledby — Point to the dialog's title heading.
  • aria-describedby — Point to the dialog's description text (optional but recommended).

Focus Management

  1. On open: Move focus to the first focusable element inside the dialog, or the close button, or the dialog itself (with tabindex="-1").
  2. While open: Trap focus within the dialog. Tab from the last focusable element should cycle back to the first.
  3. On close: Return focus to the element that triggered the dialog.

Keyboard Interaction

KeyAction
EscapeCloses the dialog (unless role="alertdialog" requires explicit choice)
TabCycles through focusable elements within the dialog
Shift + TabCycles backwards through focusable elements

The inert Attribute

Apply inert to all page content behind the dialog. This is superior to manual aria-hidden management because it also prevents mouse and keyboard interaction with background elements.

<main inert><!-- page content --></main>
<dialog open aria-labelledby="dialog-title" aria-modal="true">
  <!-- dialog content -->
</dialog>

For more on accessible overlays, see our ARIA Attributes Guide and Keyboard Accessibility Guide.

Usage Guidelines

Do's

  • ✅ Always provide a visible close mechanism — a close button, a cancel button, or both. Never trap users.
  • ✅ Use descriptive titles. "Delete project?" is better than "Are you sure?" — the title should make the dialog's purpose clear without reading the body.
  • ✅ Keep dialogs focused. One task, one dialog. If your dialog has tabs or complex navigation, it should probably be a page.
  • ✅ Match the destructive action to a destructive button variant. Red "Delete" button, not a cheerful blue one.
  • ✅ Animate the entrance (fade + slight scale) to orient users. A 200ms transition is enough — test values with our Transition Generator.

Don'ts

  • ❌ Don't open a dialog on page load. Nothing says "we don't respect your time" like an immediate popup. Cookie banners are the one painful exception.
  • ❌ Don't nest dialogs. If you need a confirmation inside a dialog, reconsider your flow. At most, use an inline confirmation within the existing dialog.
  • ❌ Don't put critical information only in a dialog. Users dismiss them reflexively. Important info should live on the page itself.
  • ❌ Don't use a dialog for success messages. Use a Toast instead.
  • ❌ Don't disable the Escape key unless you have a very good reason (e.g., an unsaved-changes warning).

Content Guidelines

  • Title: 3–8 words, action-oriented ("Confirm deletion", "Add team member")
  • Body: Keep it under 3 sentences. If you need more, the content doesn't belong in a dialog.
  • Actions: Place the primary action on the right. Use specific labels ("Delete project" not "OK").
  • Destructive dialogs: Repeat the item name in the body text. "This will permanently delete My Project."

Code Snippets

html
<!-- Dialog using native <dialog> element -->
<button type="button" id="open-dialog" aria-haspopup="dialog">
  Delete project
</button>

<dialog
  id="confirm-dialog"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-desc"
>
  <header>
    <h2 id="dialog-title">Delete project?</h2>
    <button type="button" aria-label="Close" onclick="this.closest('dialog').close()">
      <svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
        <path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"/>
      </svg>
    </button>
  </header>

  <p id="dialog-desc">
    This will permanently delete <strong>My Project</strong> and all its data.
    This action cannot be undone.
  </p>

  <footer>
    <button type="button" class="btn btn-secondary" onclick="this.closest('dialog').close()">
      Cancel
    </button>
    <button type="button" class="btn btn-destructive">
      Delete project
    </button>
  </footer>
</dialog>

<script>
  const btn = document.getElementById("open-dialog");
  const dialog = document.getElementById("confirm-dialog");
  btn.addEventListener("click", () => dialog.showModal());
</script>
tsx
import { useRef, useEffect, type ReactNode } from "react";
import { createPortal } from "react-dom";

interface DialogProps {
  open: boolean;
  onClose: () => void;
  title: string;
  description?: string;
  children: ReactNode;
  size?: "sm" | "md" | "lg";
}

export default function Dialog({
  open,
  onClose,
  title,
  description,
  children,
  size = "md",
}: DialogProps) {
  const dialogRef = useRef<HTMLDialogElement>(null);

  useEffect(() => {
    const dialog = dialogRef.current;
    if (!dialog) return;

    if (open && !dialog.open) {
      dialog.showModal();
    } else if (!open && dialog.open) {
      dialog.close();
    }
  }, [open]);

  useEffect(() => {
    const dialog = dialogRef.current;
    if (!dialog) return;
    const handleClose = () => onClose();
    dialog.addEventListener("close", handleClose);
    return () => dialog.removeEventListener("close", handleClose);
  }, [onClose]);

  const sizeClass = {
    sm: "max-w-[400px]",
    md: "max-w-[560px]",
    lg: "max-w-[720px]",
  }[size];

  return createPortal(
    <dialog
      ref={dialogRef}
      className={`dialog ${sizeClass}`}
      aria-labelledby="dialog-title"
      aria-describedby={description ? "dialog-desc" : undefined}
      onClick={(e) => {
        if (e.target === dialogRef.current) onClose();
      }}
    >
      <div className="dialog-panel">
        <header className="dialog-header">
          <h2 id="dialog-title">{title}</h2>
          <button type="button" aria-label="Close" onClick={onClose}>✕</button>
        </header>
        {description && <p id="dialog-desc">{description}</p>}
        <div className="dialog-body">{children}</div>
      </div>
    </dialog>,
    document.body,
  );
}

// Usage
function App() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <button onClick={() => setOpen(true)}>Delete project</button>
      <Dialog
        open={open}
        onClose={() => setOpen(false)}
        title="Delete project?"
        description="This action cannot be undone."
        size="sm"
      >
        <footer>
          <button onClick={() => setOpen(false)}>Cancel</button>
          <button className="btn-destructive" onClick={handleDelete}>
            Delete project
          </button>
        </footer>
      </Dialog>
    </>
  );
}

Design Systems

Cross-System Comparison

FeatureMaterial 3Shadcn/uiRadixAnt Design
ElementCustom <div> with portal<dialog> via Radix<div> with portal + focus trapCustom <div> with portal
Focus trapBuilt-inVia Radix primitivesExcellent — FocusTrap + FocusScopeBuilt-in
AnimationMaterial motion systemCSS transitions (Tailwind)BYO animationCSS transitions (Ant Motion)
Nested dialogsSupportedSupported via RadixFull stack managementSupported but discouraged
Alert variantSeparate AlertDialogAlertDialog (separate component)AlertDialog primitiveModal.confirm() static method
Close on backdropConfigurableDefault trueConfigurablemaskClosable prop
Scroll lockdocument.body overflow hiddenRemoveScroll from RadixRemoveScroll componentBody overflow hidden

Notable Approaches

Radix sets the standard for dialog accessibility. Its Dialog primitive handles focus trapping, scroll locking, and portal rendering with the inert attribute on background content. If you're building a dialog from scratch, study Radix's implementation first.

Material 3 separates "Basic Dialog" (informational) from "Full-screen Dialog" (complex tasks). The full-screen variant replaces the entire view on mobile — essentially a temporary page — which solves the "dialog inside dialog" problem elegantly.

Shadcn/ui wraps Radix primitives with Tailwind styling. The AlertDialog is a separate component from Dialog, which enforces the correct role="alertdialog" and prevents developers from accidentally using the wrong pattern for confirmations.

Ant Design offers Modal.confirm(), Modal.info(), and similar static methods that let you open a dialog imperatively (without JSX). This is convenient for one-off confirmations but makes testing harder.

The native HTML <dialog> element has reached excellent browser support (97%+ globally as of 2026). It provides showModal() for modal behavior and show() for non-modal — consider using it as your base rather than a <div> portal approach.

OverlayModalPopupLightbox