Dialogcomponent
Modal <dialog> element that opens on mount and includes a close button.
- Opens via
showModal()when mounted and closes on backdrop clicks, link/nav-button clicks, the close button, or the Escape key. - The whole dialog fades in and out in one view transition. A
<Modal>pinned to an edge leaves that layer and slides in its own. - With
onClose, a close request callsonClose()and the dialog stays open until it unmounts, so the view transition can capture it as it leaves. - Children sit in one wrapper in normal block layout,
--dialog-widthwide and centred on the screen. Content taller than the screen scrolls from its top. - Wraps content in
<Suspense>so lazy children can stream in.
A native <dialog> element opened in modal mode. It opens via showModal() when mounted and includes a built-in close button, so it works equally well mounted declaratively in the tree or pushed imperatively through a DialogsStore.
Things to know:
- Closes on a backdrop click, the Escape key, any link or
<nav>button clicked inside it, or the built-in<DialogCloseButton>(an X icon, top-right). - Children render inside a
<Suspense>boundary, so lazy content can stream in. - Children sit in one wrapper in normal block layout, so several children stack as they would on the page. The wrapper is
--dialog-widthwide (never wider than the screen) and centred on the screen. A centred<Modal>fills it. Content taller than the screen starts at the top, and the dialog scrolls. - While a dialog is open, the page behind it does not scroll. A scroll inside the dialog never passes on to the page.
Dialogonly dims the page. Its text is white (--tint-100) so it reads on the dark overlay. Wrap the content in<Modal>to give it a panel with dark text on a light surface.onClosefires when the user closes the dialog. It must unmount theDialog: clear the React state that mounts it, or (when pushed via a store) remove it from the list.DialogcallsonCloseinsidestartTransition(), and the dialog stays open until it unmounts.- The dialog animates with view transitions. The whole dialog fades in and out as one layer. A
<Modal>pinned to an edge takes its own layer and slides. - A view transition only runs for a React transition update.
DialogsStoreandonClosedo this for you. To animate a declarativeDialogas it opens, set the state that mounts it insidestartTransition(). - A browser without view transitions shows and hides the dialog at once.
- Pair with
DialogsStore,<DialogsContext>, and<Dialogs>to open dialogs imperatively from anywhere in the app.
Usage
Declarative
Mount <Dialog> directly when its lifetime matches a React state variable.
import { Dialog, Modal } from "shelving/ui";
function ConfirmDelete({ onConfirm, onClose }: { onConfirm: () => void; onClose: () => void }) {
return (
<Dialog onClose={onClose}>
<Modal>
<p>Delete this item?</p>
<button type="button" onClick={onConfirm}>Delete</button>
</Modal>
</Dialog>
);
}
// In the parent. Open inside `startTransition()` so the dialog animates in.
<button type="button" onClick={() => startTransition(() => setShowConfirm(true))}>Delete</button>
{showConfirm && <ConfirmDelete onConfirm={handleDelete} onClose={() => setShowConfirm(false)} />}Imperative
Set up the context once near the app root (see <DialogsContext> and <Dialogs>), then push a <Dialog> from anywhere with requireDialogs().
import { Modal, requireDialogs } from "shelving/ui";
function DeleteButton({ onConfirm }: { onConfirm: () => void }) {
const dialogs = requireDialogs();
const open = () =>
dialogs.show(
<Modal>
<p>Delete this item?</p>
<button type="button" onClick={onConfirm}>Delete</button>
</Modal>,
);
return <button type="button" onClick={open}>Delete</button>;
}dialogs.show() wraps the content in a <Dialog> for you, so you pass plain children rather than a <Dialog> element. It does not add a <Modal>; include one in the content if you want a panel.
Styling
Dialog paints the full-screen overlay and resets the browser's default <dialog> border, size limits, and ::backdrop. Transparent borders as wide as the screen's safe areas keep the content and the close button clear of the notch, rounded corners and home bar. The inner panel comes from its children, usually <Modal>. Override these hooks at :root (or any ancestor scope) to retheme.
| Variable | Styles | Default |
|---|---|---|
--dialog-padding | Top and bottom padding around the centred content (one length) | var(--space-normal) (16px) |
--dialog-indent | Left and right padding around the centred content (one length) | var(--space-normal) (16px) |
--dialog-width | Width of the centred content, and so of a centred <Modal> | var(--width-narrow) (36rem) |
--dialog-background | Overlay fill behind the content | var(--color-backdrop) |
--dialog-color | Text colour directly on the overlay | var(--tint-100) (white) |
--dialog-close-offset | Inset of the close button from the top-right corner | var(--space-small) (12px) |
The fade uses the fade class from <FadeTransition>, so --fade-transition-duration sets its length. It runs only as the dialog opens and closes; an open dialog stays still while other dialogs open and close.
Global tokens it reads — move these to retheme broadly: --tint-100, --width-narrow, --space-normal, --space-small, --color-backdrop, and --duration-fast (through <FadeTransition>).