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';Examples
Section titled “Examples”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.
<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>.
A form in the body
Section titled “A form in the body”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.
At most 400, 480 and 640px wide, and never wider than the window less its gutter.
Controlled
Section titled “Controlled”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.
Accessibility
Section titled “Accessibility”- The native modal
<dialog>: the browser makes the page behind it inert, moves focus inside, and closes it on Escape. PutautoFocuson 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.
Without JavaScript
Section titled “Without JavaScript”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.
Theming
Section titled “Theming”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.