Skip to main content

Dialog

A modal that takes over the page: Headless UI's Dialog portalled to <body>, focus trapped in the panel, the rest of the page inert and scroll-locked, focus restored on close. The panel is the bg-popover surface with shadow-elevated; backdrop and panel fade in and out on data-closed.

import Dialog from '@zuilib/components/dialog'

Basic

open and onClose are the whole contract: the dialog never closes on its own, it asks through onClose (Escape, backdrop click, Dialog.Close) and you flip open.

Loading example

Sizes

sm to xl cap the panel width with --dialog-size-* (24 / 32 / 42 / 56rem fallbacks). full fills the viewport: no margin, no rounding.

Loading example

Scrolling

scrollBehavior="inside" (default) caps the panel at the viewport and scrolls Dialog.Body, so the header and footer stay put. outside lets the panel grow and scrolls the page behind the backdrop.

Loading example

Non-dismissible and initial focus

dismissible={false} ignores Escape and backdrop clicks; only Dialog.Close (or your own onClose call) closes it. Mark the element to focus on open with autoFocus on any Headless UI button, or pass initialFocus.

Loading example

Props

DialogProps extends Headless UI DialogProps (role, unmount, static, …) with:

PropTypeDefaultDescription
openrequiredboolean
onCloserequired() => voidEscape and backdrop click (while dismissible) and Dialog.Close. Flip open in response
size'sm' | 'md' | 'lg' | 'xl' | 'full''md'Panel max-width from --dialog-size-*; full fills the viewport
scrollBehavior'inside' | 'outside''inside'inside scrolls Dialog.Body; outside scrolls the viewport
dismissiblebooleantruefalse ignores Escape and backdrop clicks
initialFocusRefObject<HTMLElement>Element to focus on open; overrides autoFocus
autoFocusbooleantrueFocus the data-autofocus element, else the dialog root; false focuses the first focusable element
role'dialog' | 'alertdialog''dialog'
classNamestringMerged onto the root (fixed, full-viewport layer)
containerClassNamestringMerged onto the centring layer
backdropClassNamestringMerged onto the backdrop
childrenReactNodeA Dialog.Panel

Parts

  • Dialog.Panel: the surface; clicks outside it count as backdrop clicks, so keep everything inside. className merges after the size and scroll classes.
  • Dialog.Title: <h2> by default, polymorphic through as (as="h1" for a full-screen dialog). Wired to aria-labelledby.
  • Dialog.Description: wired to aria-describedby.
  • Dialog.Header, Dialog.Body, Dialog.Footer: layout; Body is the scroll region with inside; Footer stacks below sm and right-aligns in DOM order above it, so put the primary action last.
  • Dialog.Close: label? (default 'Close'), children?, plus Headless UI Button props. No children: an X icon button pinned top-right. With children: a plain button. as={Fragment} hands the close behaviour to the single child. Works even when dismissible is false; onClick runs first and preventDefault cancels the close.

Slots

SlotElementNotes
[data-slot="dialog"]divRoot. Also data-size, data-scroll-behavior, data-dismissible, plus Headless UI's data-open / data-closed
[data-slot="dialog-backdrop"]divbg-foreground/50, fades
[data-slot="dialog-container"]divFixed layer that centres the panel; scrolls with outside
[data-slot="dialog-panel"]divAlso data-size
[data-slot="dialog-header"]div
[data-slot="dialog-title"]h2 (or as)
[data-slot="dialog-description"]p
[data-slot="dialog-body"]divBleeds through the panel padding so its scrollbar sits at the edge
[data-slot="dialog-footer"]div
[data-slot="dialog-close"]button (or as)
[data-slot="dialog-close-icon"]svgPresent on the icon-only close

Tokens

TokenDefaultUsed for
--dialog-size-sm/md/lg/xl24rem / 32rem / 42rem / 56remPanel max-width per size (fallbacks; define the token to retheme)
--dialog-paddingcalc(var(--spacing) * 6)Panel padding and the body's bleed; override this, not p-*
--popover, --popover-foreground, --borderPanel surface
--radius-lg, --shadow-elevatedPanel corners and shadow
--duration-normalBackdrop and panel transitions

Accessibility

  • Root is role="dialog" with aria-modal="true"; pass role="alertdialog" for confirmations. aria-labelledby and aria-describedby point at Dialog.Title / Dialog.Description automatically.
  • Focus moves into the dialog on open (the data-autofocus element, else the root so nothing looks pressed), is trapped with Tab / Shift+Tab, and returns to the opener on close. Everything outside is inert and the page is scroll-locked.
  • Escape closes while dismissible. A non-dismissible dialog must offer a visible way out: keep a Dialog.Close inside the panel.
  • The icon-only Dialog.Close has a visually hidden label ("Close").
  • Transitions honour prefers-reduced-motion.
  • On the server only Headless UI's portal sentinel renders; the dialog mounts on the client.
  • Drawer: the same contract sliding in from an edge.
  • Popover: non-modal, anchored to a trigger.
  • Button: the trigger and the footer actions.