/// Overlay
ModalsProvider
An imperative modal manager: open Dialogs and confirm prompts from anywhere with a hook call, no JSX in your view.
import { ModalsProvider, useModals } from "@protocore/pds";Imperative modals
Mount ModalsProvider once near your app root, then call useModals() anywhere beneath it. open(content, opts) renders any content in a Dialog; openConfirm({ title, body, onConfirm, tone }) is a ready-made confirm/cancel prompt. Both return an id you can pass to close(id); closeAll() clears the stack.
The hook API
useModals() returns four methods:
- `open(content, options?)` — render arbitrary content in a Dialog.
contentmay be a node or a function(close) => nodeso the body can dismiss itself.optionscoverstitle,showClose,dismissible, andonClose. Returns the modal id. - `openConfirm({ title, body?, onConfirm, onCancel?, tone?, confirmLabel?, cancelLabel? })` — a confirm/cancel prompt with tone-styled actions (
tonedefaults todanger). Returns the modal id. - `close(id)` — dismiss one modal by id.
- `closeAll()` — dismiss every open modal.
Each modal is a real Dialog, so the focus trap, Escape-to-close, scroll lock, and focus restoration all come for free.
When to use it
Reach for ModalsProvider when a modal is the result of an action deep in your logic — a mutation succeeded, a guard tripped, a row was clicked in a table cell renderer — and threading open state and JSX up to a render boundary would be awkward. For a modal that belongs to a specific piece of UI and is driven by a trigger button, the declarative [Dialog](/overlay/dialog) or [AlertDialog](/overlay/alert-dialog) reads better. Use this provider for the imperative, call-site-driven cases.
Usage
Do
- Mount one ModalsProvider near the root and call useModals() from anywhere below.
- Use openConfirm for destructive confirmations — it ships the tone-styled actions.
- Pass a (close) => node content function so a modal body can dismiss itself.
- Give a title, or an aria-label, so the Dialog is labelled for screen readers.
Don't
- Call useModals() outside a ModalsProvider — it throws by design.
- Reach for the imperative API when a trigger-driven Dialog would be clearer.
- Stack many modals at once; prefer one focused decision at a time.
- Rebuild confirm/cancel by hand when openConfirm already styles the actions.
Accessibility
| Keys | Action |
|---|---|
| Esc | Closes the top modal and returns focus to the opener. |
| Tab | Cycles focus forward, trapped within the modal panel. |
| Shift + Tab | Cycles focus backward, trapped within the panel. |
| Enter / Space | Activates the focused control. |
- Each modal is a Radix Dialog: role=dialog with aria-modal, focus trapped and restored.
- Supply a title (or aria-label) so the panel has an accessible name.
- openConfirm's actions are keyboard-reachable; the confirm carries the chosen tone.
- The backdrop dims and inerts the page while a modal is open.
ModalsProvider props
| Prop | Type | Default | Description |
|---|---|---|---|
children * | ReactNode | — | App subtree that can call `useModals()`. |