Skip to content
Protocore Design Systemv1.6.9

/// 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";
View as Markdown

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. content may be a node or a function (close) => node so the body can dismiss itself. options covers title, showClose, dismissible, and onClose. Returns the modal id.
  • `openConfirm({ title, body?, onConfirm, onCancel?, tone?, confirmLabel?, cancelLabel? })` — a confirm/cancel prompt with tone-styled actions (tone defaults to danger). 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

KeysAction
EscCloses the top modal and returns focus to the opener.
TabCycles focus forward, trapped within the modal panel.
Shift + TabCycles focus backward, trapped within the panel.
Enter / SpaceActivates 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

PropTypeDefaultDescription
children *ReactNodeApp subtree that can call `useModals()`.

Related

  • DialogA modal panel over a dimmed backdrop for focused tasks — forms, confirmations, and detail views that demand attention.
  • AlertDialogA modal that interrupts to confirm a consequential or destructive action — no backdrop escape, an explicit choice required.
  • PopconfirmA small confirm bubble anchored to its trigger — a title plus confirm/cancel — for low-stakes actions that still deserve a second tap.
  • SheetA full-height panel that slides in from the right for detail views and side forms that shouldn't cover the whole screen.