<!-- Protocore Design System — ModalsProvider -->
# ModalsProvider

- **Category:** Overlay (`overlay`)
- **Slug:** `overlay/modals-provider`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { ModalsProvider, useModals } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/overlay/modals-provider

> An imperative modal manager: open Dialogs and confirm prompts from anywhere with a hook call, no JSX in your view.

## 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](/overlay/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.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `children` | `ReactNode` | yes | — | App subtree that can call `useModals()`. |

## Examples

### 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.

```tsx
import { ModalsProvider, useModals, Button, Text, VStack } from "@protocore/pds";

function Toolbar() {
  const modals = useModals();
  return (
    <VStack gap={3} style={{ alignItems: "flex-start" }}>
      <Button
        variant="secondary"
        onClick={() =>
          modals.open(
            <VStack gap={3}>
              <Text size="sm" color="secondary">
                Opened imperatively — no JSX in your view, just a call.
              </Text>
            </VStack>,
            { title: "Deployment log" },
          )
        }
      >
        Open modal
      </Button>
      <Button
        variant="danger"
        onClick={() =>
          modals.openConfirm({
            title: "Delete environment?",
            body: "This removes staging-eu and all of its secrets. This cannot be undone.",
            tone: "danger",
            confirmLabel: "Delete",
            onConfirm: () => {},
          })
        }
      >
        Ask to confirm
      </Button>
    </VStack>
  );
}

export default function ModalsProviderBasics() {
  return (
    <ModalsProvider>
      <Toolbar />
    </ModalsProvider>
  );
}
```

## Do & don't

**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

**Keyboard**

| 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. |

**Notes**

- 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.

## Related

`dialog`, `alert-dialog`, `popconfirm`, `sheet`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
