Skip to content

Dialog

A modal dialog: a title, an optional description, a body and its actions, over the navy veil.

import { Dialog, dialogCloseProps, dialogTriggerProps } from '@zitdevs/ui';

These demos are server HTML with no React on the page. The buttons carry the native invoker commands, so in a browser that has them the dialogs open and close with no script at all; the page also runs enhanceDialogs(document), which closes them on a backdrop click and gives focus back to the button that opened them.

Delete “Website redesign”?

This removes the project, its files and its history for everyone on the team.

<Button label="Delete project" variant="secondary" {...dialogTriggerProps('delete')} />
<Dialog
id="delete"
title="Delete “Website redesign”?"
description="This removes the project, its files and its history for everyone on the team."
footer={
<>
<Button label="Cancel" variant="ghost" {...dialogCloseProps('delete')} />
<Button label="Delete project" onClick={remove} />
</>
}
/>

dialogTriggerProps(id) gives any button command="show-modal", commandfor={id} and aria-haspopup="dialog"; dialogCloseProps(id) gives it command="close". Spread them on a Button, an IconButton or a plain <button>.

closedby="closerequest" keeps the backdrop from closing it, so a misplaced click never loses what was typed; Escape and its buttons still do. A <form method="dialog"> closes it on submit.

Rename the project

At most 400, 480 and 640px wide, and never wider than the window less its gutter.

Small dialog

Its panel is as wide as the size says, and never wider than the window.

Close it with the button in the corner, Escape or the backdrop.

Medium dialog

Its panel is as wide as the size says, and never wider than the window.

Close it with the button in the corner, Escape or the backdrop.

Large dialog

Its panel is as wide as the size says, and never wider than the window.

Close it with the button in the corner, Escape or the backdrop.

A caller that holds the state passes open and onOpenChange. A trigger, the close buttons, Escape and the backdrop then only ask; the dialog opens and closes when open does.

'use client';
const [open, setOpen] = useState(false);
<Dialog id="delete" title="Delete project?" open={open} onOpenChange={setOpen} />;

Without open, onOpenChange still reports every change. A page script hears them as zd:dialog (DIALOG_EVENT from @zitdevs/ui/enhance), with { open } as its detail.

Prop Type Default Notes
id string required Its triggers name it. The title is ${id}-title, the description ${id}-description.
title string required An <h2>, and the dialog’s name.
description string — Under the title, and the dialog’s description.
children ReactNode — The body.
footer ReactNode — The actions, set at the end: the primary action last.
size 'sm' | 'md' | 'lg' 'md' At most 400 / 480 / 640px wide.
dismissible boolean true The close button in the corner.
closedby 'any' | 'closerequest' | 'none' 'any' any: Escape and the backdrop close it too; closerequest: Escape only; none: neither.
open boolean — Controlled: the dialog follows it once hydrated.
onOpenChange (open: boolean) => void — The state the reader asks for; uncontrolled, every change.
labels Partial<DialogLabels> English { close }. es.dialog from @zitdevs/ui/labels/es.
ref Ref<HTMLDialogElement> — The <dialog>.
anything else <dialog> attributes — On the <dialog>.

Exported: dialogTriggerProps, dialogCloseProps; types DialogProps, DialogSize, DialogLabels, DialogTriggerProps, DialogCloseProps. From @zitdevs/ui/enhance: enhanceDialogs, DIALOG_EVENT, DialogChangeDetail, DialogOptions, DialogsEnhancement.

  • The native modal <dialog>: the browser makes the page behind it inert, moves focus inside, and closes it on Escape. Put autoFocus on the element that should take focus first — the least destructive action, in a confirmation.
  • It is named by its title and described by its description. Its trigger says it opens a dialog (aria-haspopup="dialog").
  • On close, focus goes back to the button that opened it.
  • The close button in the corner is an IconButton named “Close” (labels.close).
  • The dialog fills the window and scrolls itself, its panel centred inside; a tall dialog scrolls from its top, and the page behind it does not scroll.
  • The panel fades in over 200ms with a 4px rise, not under reduced motion.

The dialog is server HTML, closed. Its triggers carry the HTML invoker commands, which browsers have shipped since 2025 (Baseline in December 2025): there, opening, closing and Escape need no script. Where the browser lacks them, or to add the backdrop click and give focus back to the opener, the enhancer runs: React runs it once the dialog hydrates; a page without React runs enhanceDialogs(document). A product that must open dialogs in older browsers without any script should link to a page instead.

surface-raised, ink, muted, body; the veil is --veil-dark; shadow-lg and radius-lg.

Use instead: a message that needs no decision → Toast; a problem in a form → Alert.