mwui
GitHub

Confirm Dialog

A promise-based confirmation dialog driven by an imperative confirm() call.

Nothing yet.

Installation

Usage

window.confirm has the right API and the wrong everything else. This is the same call — one line, inline with the code it guards — rendered as a real dialog.

import { useConfirm } from "@/components/ui/confirm-dialog";

function DeleteButton({ project }: { project: Project }) {
  const confirm = useConfirm();

  async function handleDelete() {
    const ok = await confirm({
      title: "Delete project?",
      description: `${project.name} and all of its data will be removed.`,
      confirmLabel: "Delete",
      destructive: true,
    });

    if (!ok) return;
    await deleteProject(project.id);
  }

  return <Button variant="destructive" onClick={handleDelete}>Delete</Button>;
}

No open state, no pending-action state, no dialog markup at the call site — the branch reads the way the decision actually works.

Setup

Mount the provider once, near the root:

import { ConfirmDialogProvider } from "@/components/ui/confirm-dialog";

export function App({ children }: { children: React.ReactNode }) {
  return <ConfirmDialogProvider>{children}</ConfirmDialogProvider>;
}

One dialog instance serves the whole tree. Cancelling — button, Escape, or the backdrop — resolves false; nothing rejects, so there is no try/catch around a user saying no.

Running the action inside the dialog

Pass onConfirm and the dialog stays open while it settles: the confirm button shows a spinner and both buttons lock. If it throws, the message renders in the dialog and the user can retry — closing on failure would leave them guessing whether the delete happened.

await confirm({
  title: "Revoke API key?",
  destructive: true,
  onConfirm: () => revokeKey(key.id),
});

Without onConfirm the dialog closes immediately and you run the work yourself.

Type-to-confirm

For actions that are genuinely unrecoverable, require the name:

await confirm({
  title: "Delete organization",
  description: "This cannot be undone.",
  confirmText: organization.slug,
  confirmLabel: "Delete organization",
  destructive: true,
});

The confirm button stays disabled until the text matches exactly.

Options

OptionDescription
titleRequired. The question being asked.
descriptionSupporting detail. Accepts nodes, not just strings.
confirmLabel / cancelLabelOverride the provider defaults per call.
destructiveStyle the confirm action as destructive.
confirmTextRequire this exact string to be typed first.
onConfirmRun the action inside the dialog, with pending and error states.

Behavior

  • Calling confirm while a dialog is open resolves the previous one as cancelled, so a stray second call cannot leave a promise hanging forever.
  • While onConfirm is pending, Escape and the backdrop do not close the dialog.
  • The request stays mounted through the close animation and is cleared afterwards, so the text does not vanish mid-fade.
  • Writing the label as the verb — "Delete", not "OK" — is what makes the dialog readable when it is the only thing on screen.