Skip to content
UI

Confirm Dialog

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

@zenoxai/ui/patterns/confirm-dialog Markdown

Not asked yet.

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:

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:

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.

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.

Not asked yet.

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.

Not asked yet.

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.

Not asked yet.

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.

Not asked yet.

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

Generated from the TypeScript source. className, style and children are accepted everywhere and omitted below.

ConfirmDialogHost

ConfirmDialogHost renders the dialog that `confirm()` opens. Mount it once, next to the Toaster, near the root of the app. It holds no props per call: every call passes its own options to `confirm()`.

PropTypeDefault
cancelLabelstring

Already-translated default for the cancel button.

Cancel
confirmLabelstring

Already-translated default for the confirm button.

Confirm

useConfirmDialog

useConfirmDialog returns the `confirm()` function. It exists for symmetry with hooks-first code; calling `confirm` directly works the same.

No props of its own. Accepts the props of the element it renders.

confirm

Opens the app's confirmation dialog and resolves to true when the user confirms, false when they cancel or `onConfirm` throws. Requires one <ConfirmDialogHost /> mounted near the root of the app.

PropTypeDefault
cancelLabelstring-
checkboxFieldConfirmCheckboxField-
confirmLabelstring-
confirmVariant"link" | "default" | "destructive" | "outline" | "secondary" | "ghost"

Variant of the confirm button. Use `destructive` for irreversible actions.

-
description*string-
inputFieldConfirmInputField-
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*string-