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.
Sizes
sm to xl cap the panel width with --dialog-size-* (24 / 32 / 42 /
56rem fallbacks). full fills the viewport: no margin, no rounding.
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.
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.
Props
DialogProps extends Headless UI DialogProps (role, unmount,
static, …) with:
| Prop | Type | Default | Description |
|---|---|---|---|
openrequired | boolean | — | |
onCloserequired | () => void | — | Escape 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 |
dismissible | boolean | true | false ignores Escape and backdrop clicks |
initialFocus | RefObject<HTMLElement> | — | Element to focus on open; overrides autoFocus |
autoFocus | boolean | true | Focus the data-autofocus element, else the dialog root; false focuses the first focusable element |
role | 'dialog' | 'alertdialog' | 'dialog' | |
className | string | — | Merged onto the root (fixed, full-viewport layer) |
containerClassName | string | — | Merged onto the centring layer |
backdropClassName | string | — | Merged onto the backdrop |
children | ReactNode | — | A Dialog.Panel |
Parts
Dialog.Panel: the surface; clicks outside it count as backdrop clicks, so keep everything inside.classNamemerges after the size and scroll classes.Dialog.Title:<h2>by default, polymorphic throughas(as="h1"for a full-screen dialog). Wired toaria-labelledby.Dialog.Description: wired toaria-describedby.Dialog.Header,Dialog.Body,Dialog.Footer: layout;Bodyis the scroll region withinside;Footerstacks belowsmand right-aligns in DOM order above it, so put the primary action last.Dialog.Close:label?(default'Close'),children?, plus Headless UIButtonprops. 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 whendismissibleisfalse;onClickruns first andpreventDefaultcancels the close.
Slots
| Slot | Element | Notes |
|---|---|---|
[data-slot="dialog"] | div | Root. Also data-size, data-scroll-behavior, data-dismissible, plus Headless UI's data-open / data-closed |
[data-slot="dialog-backdrop"] | div | bg-foreground/50, fades |
[data-slot="dialog-container"] | div | Fixed layer that centres the panel; scrolls with outside |
[data-slot="dialog-panel"] | div | Also data-size |
[data-slot="dialog-header"] | div | — |
[data-slot="dialog-title"] | h2 (or as) | — |
[data-slot="dialog-description"] | p | — |
[data-slot="dialog-body"] | div | Bleeds 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"] | svg | Present on the icon-only close |
Tokens
| Token | Default | Used for |
|---|---|---|
--dialog-size-sm/md/lg/xl | 24rem / 32rem / 42rem / 56rem | Panel max-width per size (fallbacks; define the token to retheme) |
--dialog-padding | calc(var(--spacing) * 6) | Panel padding and the body's bleed; override this, not p-* |
--popover, --popover-foreground, --border | Panel surface | |
--radius-lg, --shadow-elevated | Panel corners and shadow | |
--duration-normal | Backdrop and panel transitions |
Accessibility
- Root is
role="dialog"witharia-modal="true"; passrole="alertdialog"for confirmations.aria-labelledbyandaria-describedbypoint atDialog.Title/Dialog.Descriptionautomatically. - Focus moves into the dialog on open (the
data-autofocuselement, else the root so nothing looks pressed), is trapped with Tab / Shift+Tab, and returns to the opener on close. Everything outside isinertand the page is scroll-locked. - Escape closes while
dismissible. A non-dismissible dialog must offer a visible way out: keep aDialog.Closeinside the panel. - The icon-only
Dialog.Closehas a visually hiddenlabel("Close"). - Transitions honour
prefers-reduced-motion. - On the server only Headless UI's portal sentinel renders; the dialog mounts on the client.