Source: https://monarchui.dev/components/confirm-dialog/

Components · Overlays

# ConfirmDialog

Confirmation for consequential actions: danger tone, type-to-confirm, reason, async pending and error.

@rookcom/monarch-react

Component

## Type to confirm, with consequences

## Usage examples

### Required reason

### Error and retry

### From a menu with useConfirm

## Purpose

Asks the person to confirm an action before it happens, says plainly what will change, and stays open while the action runs so a failure can be shown and retried. It replaces `window.confirm` and `window.prompt`, which cannot be themed, translated or show progress.

### Use when

- Deleting, revoking, erasing or anything else that cannot be undone.
- Actions with wide effects (archive a workspace, remove a member) where a reason may be recorded.
- Irreversible actions that deserve a typed confirmation (the record’s name).

### Avoid when

- Reversible actions: do them and offer Undo in a toast instead.
- Collecting more than a short reason: use a Dialog with a form.

## Design principles

- Say what happens, not "Are you sure?": the title asks the question, the description and consequences say what changes.
- Name the button after the action ("Delete project"), never "OK" or "Yes".
- Safe by default: focus starts on Cancel, the least destructive action; outside clicks do not dismiss it; Enter in a field never confirms a mismatched phrase.
- Honest progress: while the action runs nothing dismisses the dialog, and a failure is shown in place with the reason and a chance to retry.
- The danger tone is reserved for destructive actions: a red confirm button and a calm warning icon, nothing more.

## API reference

```tsx
import { ConfirmDialog, useConfirm } from '@rookcom/monarch-react';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `ConfirmDialog` | component | client | `.mn-confirm-dialog` |
| `useConfirm` | hook | client | – |

`ConfirmDialog` is controlled (`open` + `onOpenChange`) or uncontrolled (`trigger`, `defaultOpen`). It forwards its ref to the dialog element.

`onConfirm(details)` runs the action. Return a promise for the pending state; reject (or throw) to show an error and keep the dialog open.

`useConfirm()` returns `{ confirm, element }`: render `element` once and `await confirm(options)`, which resolves with `{ reason? }` or `null` when cancelled.

Other DialogContent props (`className`, `onEscapeKeyDown`, `data-*`, …) pass through to the dialog.

Generated from the TypeScript declarations of `packages/react/src/components/overlays/confirm-dialog`.

## Properties

`reason` is `{ label, description?, placeholder?, required?, maxLength?, requiredMessage? }`; the trimmed text arrives as `details.reason`.

`pending` and `error` can be controlled when the product tracks the mutation itself.

Every visible string is a prop (`confirmLabel`, `cancelLabel`, `confirmPhraseLabel`, `confirmPhraseMismatch`, `errorMessage`, `reason.requiredMessage`) for translation.

### `ConfirmDialog`

Confirmation for consequential actions, built on Dialog with `role="alertdialog"`. Supports a danger tone, a consequence list, type-to-confirm, an optional or required reason, and async actions with pending and error states. Focus starts on Cancel, the least destructive action, and nothing dismisses the dialog while the action runs.

| Prop | Type | Description |
| --- | --- | --- |
| `onConfirm` required | `(details: ConfirmDetails) => unknown` | Runs the action. Return a promise to keep the dialog open in a pending state until it settles: on success it closes (see `closeOnConfirm`), on rejection the error is shown inside the dialog and the person can retry. |
| `title` required | `ReactNode` | The question, e.g. "Delete this project?". Becomes the accessible name. |
| `asChild` | `boolean` |  |
| `cancelLabel` | `ReactNode` | Default "Cancel". |
| `children` | `ReactNode` | Extra content between the description and the fields, e.g. a step-up slot. |
| `className` | `string` |  |
| `closeLabel` | `string \| null` | Accessible label of the built-in close button. Pass null to hide the button. |
| `closeOnConfirm` | `boolean` | Close after `onConfirm` succeeds. Default `true`. |
| `closeOnOutsideClick` | `boolean` | Whether a click outside the dialog closes it. Default `true`; turn it off for forms with typed values so a stray click does not lose them. Escape and the close button still work. |
| `confirmLabel` | `ReactNode` | Label of the confirming button. Name the action ("Delete project"), not "OK". Default "Confirm". |
| `confirmPhrase` | `string` | Type-to-confirm: the confirm button stays disabled until this exact phrase (case-sensitive, surrounding spaces ignored) is typed. |
| `confirmPhraseLabel` | `ReactNode` | Label of the phrase field. Default: Type "<phrase>" to confirm. |
| `confirmPhraseMismatch` | `ReactNode` | Shown when the person submits a phrase that does not match. |
| `consequences` | `ReactNode[]` | Concrete consequences, shown as a list ("12 tasks will be deleted"). |
| `defaultOpen` | `boolean` | Uncontrolled initial open state. |
| `description` | `ReactNode` | What will happen. Becomes the accessible description. |
| `error` | `ReactNode` | Controlled error message, shown above the actions. |
| `errorMessage` | `((error: unknown) => ReactNode)` | Turns a rejected `onConfirm` into a message. Default: the Error's message, or a generic line. |
| `escapeGuard` | `boolean` | Ignore Escape while focus is on a control whose own popup is open (a combobox or menu button with `aria-expanded="true"`), so that press only closes the popup. Default `true`. |
| `fullScreenOnPhone` | `boolean` | Below 640px wide, fill the whole screen instead of floating with a margin. |
| `id` | `string` |  |
| `initialFocus` | `"cancel" \| "field"` | Where focus goes on open. `cancel` (default) is the least destructive action; `field` focuses the phrase or reason field when there is one. |
| `onCancel` | `(() => void)` | Called when the dialog is dismissed without confirming. |
| `onOpenChange` | `((open: boolean) => void)` |  |
| `open` | `boolean` | Controlled open state. |
| `pending` | `boolean` | Controlled pending state, for actions the product tracks itself. |
| `reason` | `ConfirmReasonField` | Ask for a reason (optional or required); it is passed to `onConfirm`. |
| `size` | `"sm" \| "md" \| "lg" \| "xl"` | Dialog width. Default `sm`. |
| `tone` | `"danger" \| "default"` | `danger` for destructive or irreversible actions: red confirm button and a warning icon. |
| `trigger` | `ReactElement<unknown, string \| import("/home/runner/work/monarch/monarch/node_modules/@types/react/index").JSXElementConstructor<any>>` | The element that opens the dialog (rendered with `asChild`), e.g. a Button. |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `tone` | `'default'``'danger'` | `danger` uses the danger Button and a warning icon in the header. Exposed as `data-tone`. |
| `confirmPhrase` | `'undefined'``'string'` | Type-to-confirm. The confirm button is disabled until the phrase matches exactly (case-sensitive; surrounding spaces ignored). |
| `reason` | `'undefined'``'optional'``'required'` | A labelled textarea; `required` keeps the confirm button disabled until it has text. |
| `size` | `'sm'``'md'``'lg'``'xl'` | Dialog width; default `sm`. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Blocked | `disabled (confirm button)` | The phrase does not match or a required reason is empty. |
| Mismatch | `aria-invalid (field)` | Pressing Enter with a wrong phrase, or confirming without a required reason, shows a field error. |
| Pending | `aria-busy (dialog and confirm button), data-busy` | The confirm button shows a spinner and keeps focus; Cancel is disabled; Escape and outside clicks are ignored; fields are read-only. |
| Error | `role="alert"` | A danger Alert above the actions, also linked to the confirm button with aria-describedby. The person can retry or cancel. |

## Interaction behaviour

- Opening focuses Cancel (`initialFocus="cancel"`), or the phrase or reason field with `initialFocus="field"`.
- Confirming calls `onConfirm`. A synchronous action closes the dialog at once; a promise keeps it pending until it settles.
- On success it closes (unless `closeOnConfirm={false}`); on failure it stays open with the error and the confirm button ready to retry.
- Escape and Cancel call `onCancel` and close it, except while pending.
- Every opening starts clean: typed phrase, reason and old errors are cleared.
- From a DropdownMenu item, call `confirm()` from `useConfirm` in `onSelect`; focus returns to the menu trigger afterwards.

## Keyboard support

| Keys | Action |
| --- | --- |
| Tab / Shift+Tab | Moves between the fields, Cancel and the confirm button; focus stays inside. |
| Enter (phrase field) | Confirms when the phrase matches; otherwise shows the mismatch error. |
| Enter / Space (button) | Activates Cancel or the confirm button. |
| Escape | Cancels and returns focus to the trigger; ignored while pending. |

## Accessibility

Role

alertdialog (aria-modal)

Accessible name

title

;

description

becomes the accessible description.

- Initial focus is on the least destructive action (APG alert dialog pattern).
- The phrase field is labelled with the exact phrase and has autocomplete, autocorrect and spellcheck off.
- Field errors are announced politely and linked to the field; an action error is announced assertively (`role="alert"`).
- The pending confirm button keeps focus and sets `aria-busy`, so keyboard users do not lose their place.
- The warning icon is decorative (`aria-hidden`); the title carries the meaning.

## Responsive behaviour

- Default width 24rem (`sm`), never wider than the screen minus 2rem.
- The footer wraps its buttons on narrow screens; long phrases wrap inside the label.
- Long consequence lists scroll inside the body while the actions stay visible.

## Theme support

- Dialog surface tokens, danger tokens for the icon and button, and the muted surface behind the phrase.
- Light, dark and more-contrast themes apply automatically; motion follows `prefers-reduced-motion`.

## Do and don't

### Do

- Describe the concrete result, with counts, in `description` or `consequences`.
- Use `confirmPhrase` with the record’s own name for irreversible, high-impact actions.
- Return the mutation’s promise from `onConfirm` so progress and failure show in the dialog.
- Throw errors with a message the person can act on, or map them with `errorMessage`.

### Don't

- Do not confirm reversible actions; offer Undo instead.
- Do not use "OK" / "Cancel" or a vague "Are you sure?" title.
- Do not use the danger tone for non-destructive actions.
- Do not close the dialog yourself before the action has finished.

## Related components

- [Dialog](https://monarchui.dev/components/dialog/): The base it is built on, for custom content.
- [DropdownMenu](https://monarchui.dev/components/dropdown-menu/): Destructive menu items that open a confirmation.
- [Toast](https://monarchui.dev/components/toast/): Undo for reversible actions instead of a confirmation.
- [Alert](https://monarchui.dev/components/alert/): Shows the error inside the dialog.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [Button](https://monarchui.dev/components/button/), [Alert](https://monarchui.dev/components/alert/), [Field](https://monarchui.dev/components/form-field/), [Input](https://monarchui.dev/components/text-field/), [Dialog](https://monarchui.dev/components/dialog/) |

## Testing coverage

- Opens as an alertdialog with focus on Cancel; axe passes; Escape cancels (onCancel) and restores focus.
- Keeps focus inside and ignores outside clicks.
- Confirms a synchronous action and closes.
- Type-to-confirm: blocked until the exact phrase; Enter with a mismatch marks the field invalid and describes the error; surrounding spaces are ignored.
- Async: pending sets aria-busy, keeps focus on the confirm button, disables Cancel and blocks Escape; success closes.
- Async: a rejection shows the error, links it to the confirm button, keeps it open and allows retry.
- Generic message for non-Error rejections; `errorMessage` maps errors; a synchronous throw is shown too.
- Required reason blocks until filled and is passed trimmed; an empty optional reason is omitted.
- Consequences render as a list; children come before the fields.
- Controlled `pending` and `error`; controlled `open` with `closeOnConfirm={false}`.
- Starts clean on every opening.
- useConfirm resolves with the details on confirm and null on cancel, and runs async work inside the dialog first.
- useConfirm from a DropdownMenu item: focus lands on Cancel, and returns to the menu trigger after closing.
- Renders on the server (closed and open).

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: gallery/confirm-dialog and overlays/confirm-dialog-light (open); Cross-browser axe: /components/confirm-dialog |

Tests: `packages/react/src/components/overlays/confirm-dialog`

## All example code

### Type to confirm, with consequences

```tsx
import { Button, ConfirmDialog } from '@rookcom/monarch-react';

const wait = (ms: number) => new Promise<void>((resolve) => window.setTimeout(resolve, ms));

function DeleteProject() {
  return (
    <ConfirmDialog
      trigger={<Button variant="danger">Delete project</Button>}
      tone="danger"
      title="Delete this project?"
      description="This cannot be undone."
      consequences={[
        '48 tasks and their comments are deleted',
        '6 people lose access',
        'Shared links stop working',
      ]}
      confirmPhrase="q3-launch"
      confirmLabel="Delete project"
      onConfirm={() => wait(1200)}
    />
  );
}

<DeleteProject />
```

### Required reason

```tsx
import { Button, ConfirmDialog } from '@rookcom/monarch-react';

const wait = (ms: number) => new Promise<void>((resolve) => window.setTimeout(resolve, ms));

function DeclineRequest() {
  return (
    <ConfirmDialog
      trigger={<Button>Decline request</Button>}
      title="Decline this access request?"
      description="The requester is told by email."
      reason={{
        label: 'Reason',
        description: 'Shown to the requester.',
        required: true,
        placeholder: 'For example: use the shared team account instead',
      }}
      confirmLabel="Decline request"
      onConfirm={() => wait(800)}
    />
  );
}

<DeclineRequest />
```

### Error and retry

```tsx
import { Button, ConfirmDialog } from '@rookcom/monarch-react';

const wait = (ms: number) => new Promise<void>((resolve) => window.setTimeout(resolve, ms));

function RevokeWithError() {
  return (
    <ConfirmDialog
      trigger={<Button>Revoke key</Button>}
      tone="danger"
      title="Revoke this API key?"
      description="Requests signed with it start failing straight away."
      confirmLabel="Revoke key"
      onConfirm={async () => {
        await wait(800);
        throw new Error('The key is in use by a running export. Try again when it finishes.');
      }}
    />
  );
}

<RevokeWithError />
```

### From a menu with useConfirm

```tsx
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, useConfirm } from '@rookcom/monarch-react';

// MoreIcon, TrashIcon: demo icons from the docs site; use your own.

const wait = (ms: number) => new Promise<void>((resolve) => window.setTimeout(resolve, ms));

function RowActions() {
  const { confirm, element } = useConfirm();
  return (
    <>
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <Button trailingIcon={<MoreIcon />}>Note actions</Button>
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuItem>Pin</DropdownMenuItem>
          <DropdownMenuItem
            tone="danger"
            icon={<TrashIcon />}
            onSelect={async () => {
              const result = await confirm({
                tone: 'danger',
                title: 'Delete this note?',
                description: 'It is removed for everyone on the account.',
                confirmLabel: 'Delete note',
                onConfirm: () => wait(600),
              });
              if (result) {
                // The note is deleted; update the list here.
              }
            }}
          >
            Delete
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
      {element}
    </>
  );
}

<RowActions />
```
