# Confirm Dialog

Asks the user to confirm an action before it runs, with one awaited call to confirm().

```tsx
import * as React from "react"
import { Button } from "@zenoxai/ui/components/button"
import { confirm } from "@zenoxai/ui/patterns/confirm-dialog"

function ConfirmDialogDemo() {
  const [result, setResult] = React.useState<boolean | null>(null)

  async function dismissAlert() {
    const confirmed = await confirm({
      title: "Dismiss this alert?",
      description:
        "The alert moves to Dismissed and stops notifying the on-call analyst.",
      confirmLabel: "Dismiss alert",
    })
    setResult(confirmed)
  }

  return (
    <div className="flex flex-col items-center gap-3">
      <Button variant="outline" onClick={dismissAlert}>
        Dismiss alert
      </Button>
      <p className="text-sm text-text-secondary">
        {result === null
          ? "Not asked yet."
          : `confirm() resolved to ${result}.`}
      </p>
    </div>
  )
}
```

## Usage

Mount one `ConfirmDialogHost` near the root of the app, next to the `Toaster`:

```tsx
import { Toaster } from "@zenoxai/ui/components/sonner"
import { ConfirmDialogHost } from "@zenoxai/ui/patterns/confirm-dialog"

export function AppProviders({ children }: { children: React.ReactNode }) {
  return (
    <>
      {children}
      <Toaster />
      <ConfirmDialogHost confirmLabel="Confirm" cancelLabel="Cancel" />
    </>
  )
}
```

Then call `confirm()` from any event handler. It opens the dialog and resolves to `true` when
the user confirms, `false` when they cancel, press Escape or `onConfirm` throws:

```tsx
import { confirm } from "@zenoxai/ui/patterns/confirm-dialog"

async function dismissAlert() {
  const confirmed = await confirm({
    title: "Dismiss this alert?",
    description:
      "The alert moves to Dismissed and stops notifying the on-call analyst.",
    confirmLabel: "Dismiss alert",
  })
  if (!confirmed) return
  // ...
}
```

The host takes the default button labels, already translated. Everything else comes with each
call. Mount exactly one host: every host renders the same dialog, so a second one opens a second
copy on top of the first. This page mounts one host for all of its examples.

Ask for confirmation only before actions that are destructive, expensive or hard to undo. For
an action that can be undone, run it and offer Undo in a toast. For a task that needs a form,
use a [Dialog](/components/dialog).

Write the `title` as the question, naming the action and the object ("Delete tenant
acme-prod?"), and the `description` as what happens and whether it can be undone. Repeat the
verb of the title in `confirmLabel` ("Delete tenant"), never "OK" or "Yes". The full list of
options is in the API reference below.

A second `confirm()` while a dialog is open cancels the first one, which resolves to `false`.

## Examples

### Destructive

Set `confirmVariant: "destructive"` for an action that deletes data or cannot be undone, and say
so in the description.

```tsx
import * as React from "react"
import { Button } from "@zenoxai/ui/components/button"
import { confirm } from "@zenoxai/ui/patterns/confirm-dialog"

function ConfirmDialogDestructive() {
  const [result, setResult] = React.useState<boolean | null>(null)

  async function deleteReport() {
    const confirmed = await confirm({
      title: "Delete this report?",
      description:
        "Monthly exposure report, September 2026 will be deleted for everyone in the tenant. This cannot be undone.",
      confirmLabel: "Delete report",
      confirmVariant: "destructive",
    })
    setResult(confirmed)
  }

  return (
    <div className="flex flex-col items-center gap-3">
      <Button variant="destructive" onClick={deleteReport}>
        Delete report
      </Button>
      <p className="text-sm text-text-secondary">
        {result === null
          ? "Not asked yet."
          : `confirm() resolved to ${result}.`}
      </p>
    </div>
  )
}
```

### Type to confirm

For a destructive action on a named resource, such as a tenant or an integration, pass
`inputField.expectedValue`. The dialog shows the value after the field label as a monospace chip
on a muted fill, so it reads as the literal to copy (one click selects it whole). Write the
label as the instruction without the value, such as "Type the tenant name to confirm". The
dialog renders a single-line field and keeps the confirm button disabled until the trimmed input equals the
value exactly; Enter then confirms. While `onConfirm` runs, the confirm button shows a spinner
and is marked busy.

```tsx
import * as React from "react"
import { Button } from "@zenoxai/ui/components/button"
import { confirm } from "@zenoxai/ui/patterns/confirm-dialog"

function ConfirmDialogTypeToConfirm() {
  const [result, setResult] = React.useState<boolean | null>(null)

  async function deleteTenant() {
    const confirmed = await confirm({
      title: "Delete tenant acme-prod?",
      description:
        "Every asset, alert and report of this tenant will be deleted. This cannot be undone.",
      confirmLabel: "Delete tenant",
      confirmVariant: "destructive",
      inputField: {
        label: "Type the tenant name to confirm",
        expectedValue: "acme-prod",
      },
    })
    setResult(confirmed)
  }

  return (
    <div className="flex flex-col items-center gap-3">
      <Button variant="destructive" onClick={deleteTenant}>
        Delete tenant
      </Button>
      <p className="text-sm text-text-secondary">
        {result === null
          ? "Not asked yet."
          : `confirm() resolved to ${result}.`}
      </p>
    </div>
  )
}
```

### With an option

`checkboxField` adds one checkbox, for a choice that only makes sense at the moment of
confirming. The promise resolves to a boolean, so read the checkbox in `onConfirm`.

```tsx
import * as React from "react"
import { Button } from "@zenoxai/ui/components/button"
import { confirm } from "@zenoxai/ui/patterns/confirm-dialog"

function ConfirmDialogWithCheckbox() {
  const [message, setMessage] = React.useState("Not asked yet.")

  async function removeAsset() {
    let deleteHistory = false
    const confirmed = await confirm({
      title: "Remove web-prod-03?",
      description: "The asset stops being scanned and leaves every report.",
      confirmLabel: "Remove asset",
      confirmVariant: "destructive",
      checkboxField: { label: "Also delete its scan history" },
      onConfirm: async ({ checked }) => {
        deleteHistory = checked
      },
    })
    if (!confirmed) setMessage("confirm() resolved to false.")
    else if (deleteHistory) setMessage("Removed, with its scan history.")
    else setMessage("Removed. Scan history kept.")
  }

  return (
    <div className="flex flex-col items-center gap-3">
      <Button variant="outline" onClick={removeAsset}>
        Remove asset
      </Button>
      <p className="text-sm text-text-secondary">{message}</p>
    </div>
  )
}
```

### Async action

Run the action in `onConfirm` to keep the dialog open while it runs. Both buttons are disabled
and Escape is ignored until the promise settles. If `onConfirm` throws, the dialog closes and
`confirm()` resolves to `false`; report the error from inside `onConfirm`, for example with a
toast.

```tsx
import * as React from "react"
import { Button } from "@zenoxai/ui/components/button"
import { confirm } from "@zenoxai/ui/patterns/confirm-dialog"

function ConfirmDialogAsync() {
  const [result, setResult] = React.useState<boolean | null>(null)

  async function revokeKey() {
    const confirmed = await confirm({
      title: "Revoke this API key?",
      description:
        "Integrations that use the key stop sending events until you add a new one.",
      confirmLabel: "Revoke key",
      confirmVariant: "destructive",
      onConfirm: async () => {
        // Stands in for the API call. Throw to keep the key and resolve false.
        await new Promise((resolve) => setTimeout(resolve, 1500))
      },
    })
    setResult(confirmed)
  }

  return (
    <div className="flex flex-col items-center gap-3">
      <Button variant="outline" onClick={revokeKey}>
        Revoke API key
      </Button>
      <p className="text-sm text-text-secondary">
        {result === null
          ? "Not asked yet."
          : `confirm() resolved to ${result}.`}
      </p>
    </div>
  )
}
```

## Accessibility

- The dialog is an alert dialog: it traps focus, does not close on an outside click, and on
  close returns focus to the element that had it before, usually the button that called
  `confirm()`.
- The title and description are the dialog's accessible name and description. Write both as
  full sentences; the title alone must make sense.
- While `onConfirm` runs, the buttons are only disabled. If the action takes more than a moment,
  report progress or the result with a toast so screen reader users hear it.

## API reference

`import { ... } from "@zenoxai/ui/patterns/confirm-dialog"`

### ConfirmDialogHost

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| cancelLabel | `string` | `Cancel` | Already-translated default for the cancel button. |
| confirmLabel | `string` | `Confirm` | Already-translated default for the confirm button. |

### useConfirmDialog

No props of its own.

### confirm

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| cancelLabel | `string` |  |  |
| checkboxField | `ConfirmCheckboxField` |  |  |
| confirmLabel | `string` |  |  |
| confirmVariant | `"link" \| "default" \| "destructive" \| "outline" \| "secondary" \| "ghost"` |  | Variant of the confirm button. Use `destructive` for irreversible actions. |
| description (required) | `string` |  |  |
| inputField | `ConfirmInputField` |  |  |
| onConfirm | `((input: { text: string; checked: boolean; }) => Promise<void>)` |  | Runs while the dialog shows a pending state. If it throws, the dialog closes and the promise resolves to false; report the error from here. |
| title (required) | `string` |  |  |

