Practical guide
Name every dialog
Prefer a visible heading connected by ariaLabelledBy; connect explanatory text with ariaDescribedBy. If no visible title exists, provide ariaLabel. Use role: 'alertdialog' only for urgent decisions requiring immediate attention.
await display({
role: 'alertdialog',
ariaLabelledBy: 'discard-title',
ariaDescribedBy: 'discard-description',
render: (close) => (
<section>
<h2 id="discard-title">Discard changes?</h2>
<p id="discard-description">Unsaved edits will be lost.</p>
<button onClick={() => close(false)}>Keep editing</button>
<button onClick={() => close(true)}>Discard</button>
</section>
),
});The inline adapter traps focus in the top modal, focuses the first control (or panel), restores prior focus, and locks body scroll. Native <dialog> handles focus containment in top-layer mode. Set autoFocus or restoreFocus to false only when your application replaces that behavior deliberately.
Choose a transport
Use the default transport: 'inline' for consistent package-controlled positioning, backdrop, and motion. Use transport: 'top-layer' when the dialog must escape clipping and stacking contexts:
await display({
transport: 'top-layer',
ariaLabel: 'Account settings',
render: (close) => <SettingsPanel onDone={() => close()} />,
});Top-layer transport requires browser support for native <dialog> and showModal(). It does not include a polyfill.
Style the panel and backdrop
className and style target the panel or <dialog>. backdropClassName and backdropStyle target the inline backdrop; top-layer mode applies backdrop styling through its generated ::backdrop rule. Package motion uses 200 ms fade/scale animations and honors prefers-reduced-motion by removing promptly.
Use scoped and global commands intentionally
Inside the provider, prefer useModal() so ownership is visible. The module-level modal facade is useful outside React components, but requires one mounted default <ModalProvider>:
<ModalProvider><AppRoutes /></ModalProvider>;
const id = modal.open({ ariaLabel: 'Working', render: () => <Progress /> });
modal.close(id);For multiple independent providers, pass each a distinct createOverlayStore() and use hooks inside its tree; the global facade always targets globalModalStore.
Keep render callbacks pure
Do not call hooks or run side effects inside render. Return a component that owns hooks instead: render: (close) => <EditorDialog onClose={close} />.