Skip to content
Monogem

Component · Containers & Overlays

Dialog

Overview

A modal overlay that interrupts the current flow until the user resolves it — the page behind it becomes inert. Dialog's modal behavior — focus trapping, focus restoration, background inertness, and keyboard dismissal — is specified under Focus trap and restoration.

Anatomy

Overlay (backdrop) → Container → Title → Description → Body → Actions → Close control.

  • Overlay — a scrim behind the dialog that visually and functionally separates it from the page. See Overlay scrim for the explicit value the scrim uses.
  • Container — the dialog surface itself.
  • Title — required; names what the dialog is about (Heading 4 recipe).
  • Description — optional supporting sentence under the title (Body small, --muted-foreground).
  • Body — the dialog's main content; free-form.
  • Actions — a footer row of one or more Buttons; see Action ordering.
  • Close control — an optional ✕ icon-button, top-right. Optional because Escape and the Actions row may already provide an exit; include it when the dialog has no clear default "cancel" action otherwise (e.g. a pure informational dialog with only one "OK" action).

Live Example

Standard dialog — a short form

Destructive confirmation — overlay-click disabled

Long content — only the body scrolls

Code Example

dialog-showcase.tsx
"use client";

import { X } from "lucide-react";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogBody,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";

/**
* Live Dialog example — open one and the page behind it goes inert: `Tab` and
* `Shift+Tab` cycle only inside the dialog, `Escape` closes it, and focus
* returns to the trigger on close.
*
* - Standard dialog: a short form at the default size. Overlay-click and
* `Escape` both dismiss; there is no `✕` because Cancel is a clear exit.
* - Destructive confirmation: `role="alertdialog"`, `dismissible={false}` so an
* accidental overlay click can't discard the decision, Title names the
* action, Description states the consequence, and Cancel (first in the DOM,
* so it takes initial focus) is paired with a `destructive` confirm.
* - Long content: Title and the footer stay fixed while only the body scrolls;
* the container never grows past the viewport.
*/
export function DialogShowcase() {
return (
<div className="flex flex-col gap-8">
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Standard dialog — a short form
</h3>
<Dialog>
<DialogTrigger>
<Button variant="outline">Rename workspace</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Rename workspace</DialogTitle>
<DialogDescription>
This changes the name everywhere it appears for your team.
</DialogDescription>
</DialogHeader>
<DialogBody>
<div className="flex flex-col gap-2">
<Label htmlFor="rename-workspace">Workspace name</Label>
<Input id="rename-workspace" defaultValue="Acme Inc." />
</div>
</DialogBody>
<DialogFooter>
<DialogClose>
<Button variant="ghost">Cancel</Button>
</DialogClose>
<DialogClose>
<Button>Save</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</section>

<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Destructive confirmation — overlay-click disabled
</h3>
<Dialog role="alertdialog" dismissible={false}>
<DialogTrigger>
<Button variant="destructive">Delete project</Button>
</DialogTrigger>
<DialogContent size="small">
<DialogHeader>
<DialogTitle>Delete project?</DialogTitle>
<DialogDescription>
This permanently deletes “Migration plan” and all of its tasks.
This can’t be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose>
<Button variant="ghost">Cancel</Button>
</DialogClose>
<DialogClose>
<Button variant="destructive">Delete</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</section>

<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Long content — only the body scrolls
</h3>
<Dialog>
<DialogTrigger>
<Button variant="outline">Review terms</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Updated terms of service</DialogTitle>
<DialogDescription>
Review the changes below before continuing.
</DialogDescription>
<DialogClose>
<Button
variant="ghost"
size="icon"
aria-label="Close"
className="absolute right-4 top-4"
>
<X />
</Button>
</DialogClose>
</DialogHeader>
<DialogBody>
<div className="flex flex-col gap-4">
{Array.from({ length: 12 }, (_, i) => (
<p key={i}>
Section {i + 1}. The body region scrolls internally once its
content exceeds the available height, while the title,
description, and footer stay pinned in place. The dialog
container itself is capped and never grows to fit long
content.
</p>
))}
</div>
</DialogBody>
<DialogFooter>
<DialogClose>
<Button variant="ghost">Decline</Button>
</DialogClose>
<DialogClose>
<Button>Accept</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</section>
</div>
);
}

States

StateLook
ClosedNot rendered
OpenOverlay visible, container --background fill, --radius-xl, --shadow-lg

Usage Guidance

Purpose and interruption threshold

Reserve Dialog for moments that genuinely need to block everything else: a decision that must be made before continuing (a destructive confirmation), a short task that must complete or be explicitly cancelled, or a critical message the user can't be allowed to miss. Every Dialog is friction by design — opening one should be justified by that interruption actually being necessary, not by convenience.

When not to use it

  • Supplementary, non-blocking content — that's Popover.
  • A single-line clarification — that's Tooltip.
  • A focused secondary task that benefits from more page context around it, or a mobile-style bottom-anchored flow — that's Sheet.
  • A substantial, multi-step, or deep-linkable task — see When to use a full page instead.

Overlay scrim

No dedicated overlay/scrim token exists in Color. Dialog uses an explicit value instead of an approximate --foreground-at-opacity lever:

rgb(0 0 0 / 48%)
  • The same neutral black scrim is used in both light and dark mode — it doesn't flip per theme the way a --foreground-derived value would.
  • This is an explicit value, not a semantic token. The 48% opacity separates the dialog from the page behind it without obscuring context so completely that the interruption feels jarring.
  • A dedicated --overlay or --scrim semantic token may be added to Color in a future release; nothing here depends on it.

Dismissal: Escape and overlay click

Escape and overlay-click are documented separately — they are not automatically identical, and overlay-click dismissal is conditional on the dialog's risk level.

Dismissal methodDefault behavior
EscapeCloses the dialog by default
Click on overlayCloses standard, low-risk dialogs by default

Overlay-click dismissal must be disabled (the click does nothing) for:

  • Destructive confirmations (see Destructive confirmations)
  • Unsaved-change flows, where an accidental outside click would silently discard work
  • Critical acknowledgements the user must not be able to dismiss without engaging
  • Any other interaction where accidental dismissal creates meaningful risk

When overlay-click dismissal is disabled, an explicit close or cancel action (the Actions row, or the optional Close control) must remain available — disabling overlay-click is never a reason to leave a dialog with no way out.

Escape follows the same risk-based judgment on its own terms: a destructive confirmation or unsaved-changes flow may also choose to intercept Escape to confirm first, the same kind of per-instance call Radio Group documents for default selection — but that's a decision made independently, not inherited automatically from the overlay-click rule above.

Destructive confirmations

  • Title names the action directly ("Delete project?"), not a generic "Are you sure?".
  • Description states the consequence, including whether it's reversible.
  • Actions row pairs a neutral Cancel with a Destructive-variant confirm button, clearly labeled with the action itself ("Delete", not "OK").
  • Default focus lands on Cancel, not the destructive action — the same safe-default reasoning Radio Group applies to never preselecting a high-stakes option: an accidental Enter press should never confirm something destructive. This focus default is specific to destructive confirmations, not a rule for every Dialog.
  • Overlay-click dismissal is disabled — see Dismissal: Escape and overlay click.

Action ordering

The primary/confirm action sits at the right end of the Actions row; Cancel (or another lower-emphasis action) sits to its left. This matches the prevailing convention this system's other right-aligned action rows already use and keeps the action a user is most likely to take predictable in position.

Long content and scrolling

Title, Description, and Actions stay fixed; only the Body scrolls internally once its content exceeds the available height. The dialog container itself doesn't grow past its capped size (see Responsive sizing) to accommodate long content.

When to use a full page instead

Use a dedicated page (a Layout "Narrow form" or "App shell" pattern) instead of a Dialog when the content is a multi-step flow, needs its own URL for deep-linking or sharing, or simply needs more room than a bounded modal can reasonably offer without becoming its own scrollable sub-application.

Tokens

Dialog uses existing Monogem semantic tokens to stay consistent with the design system. The one lever that isn't a clean token fit is the Overlay scrim.

TokenWhere used
--background / --foregroundContainer fill + text
--muted-foregroundDescription text
--borderContainer edge, useful on dark mode per Shadows
--radius-xlCorner radius — Radius explicitly names "Modals, large surfaces"
--shadow-lgElevation — Shadows names "Modals, dialogs, sheets"
--ringFocus ring on interactive content, and on the close control
--scale-6Container padding
--scale-4Gap between Title/Description/Body/Actions; also the mobile inset on each side below --breakpoint-sm
--breakpoint-sm / --breakpoint-md / --breakpoint-lgContainer max-width — Small / Default / Large (see Responsive sizing)

The overlay scrim is an explicit rgb(0 0 0 / 48%) value, not a token — see Overlay scrim.

Do / Don’t

Do

  • Reserve Dialog for genuinely blocking moments.
  • Default focus to Cancel for any destructive confirmation.
  • Keep the Title, Description, and Actions fixed while the Body scrolls.
  • Disable overlay-click dismissal for destructive confirmations, unsaved-change flows, and critical acknowledgements, while keeping an explicit close or cancel action available.

Don’t

  • Stack a Dialog on top of another open Dialog — resolve or close the first before opening a second.
  • Fire a Toast from inside an open Dialog — the page behind it (the Toast viewport included) is inert, so the toast is covered by the scrim and can't be reached or announced. Close the Dialog first, or report inline. See the Toast known limitation.
  • Use Dialog for a long, multi-step flow — use a full page.
  • Make the destructive action the default-focused button.
  • Allow overlay-click dismissal on a destructive confirmation, unsaved-change flow, or critical acknowledgement.

Accessibility

  • role="dialog" (or alertdialog for a critical, must-acknowledge message) with aria-modal="true".
  • aria-labelledby references the Title's id; aria-describedby references the Description's id when present.
  • Focus trap and restoration as described above — required, not optional, for keyboard and screen-reader users alike.
  • The rest of the page is marked inert (e.g. aria-hidden / inert on background content) while the dialog is open, so assistive tech can't navigate into content the user can't actually reach.

Focus trap and restoration

On open, focus moves into the dialog — onto its first focusable element, or the dialog container itself when no field inside is an obviously correct first stop. While open, Tab/Shift+Tab cycle only among the dialog's own focusable elements; focus cannot land on anything in the page behind it. On close, focus returns to the element that opened the dialog.

This is a hard runtime requirement, not a visual one: keyboard and screen-reader users depend on it.

Responsive Behavior

Three sizes, each capped at one of Grids' existing breakpoint tokens — reused directly rather than inventing bespoke width values — so the hierarchy is unambiguous: small < default < large.

SizeMax-widthMobile (below --breakpoint-sm)Use forInternal scrolling
Small--breakpoint-sm (640px)Full viewport width, --scale-4 inset each sideShort confirmations, single-message acknowledgementsBody scrolls only if content still exceeds the viewport height at full width
Default--breakpoint-md (768px)Full viewport width, --scale-4 inset each sideMost dialogs — a short form, a focused task with a few fieldsBody scrolls once content exceeds the container's capped height
Large--breakpoint-lg (1024px)Full viewport width, --scale-4 inset each sideDenser content — multi-field forms, an embedded table or listBody scrolls once content exceeds the container's capped height

Every size converges to the same full-width, inset mobile presentation below --breakpoint-sm — converging toward Sheet's mobile footprint. Version 1 keeps a full-width, inset Dialog at that point rather than converting it to a Sheet — the two stay distinct components.

Related Components / Patterns

  • Sheet — the edge-attached modal counterpart.
  • Popover — non-modal supplemental content.
  • Toast — transient feedback that doesn’t interrupt.