Layers

Confirmer

Overview

Imperative confirmation dialog with async action support, loading state, and inline error display. Call confirm() to open the dialog and optionally run an async action.

Install

Configure your registry in components.json:

{
  "registries": {
    "@uxio": "https://ui.uxio.dev/r/styles/{style}/{name}.json"
  }
}

Then run:

npx shadcn@latest add @uxio/confirmer

Configuration

confirm() accepts ConfirmerOptions:

PropTypeDefaultDescription
titlestringDialog title (required).
descriptionstringOptional body HTML (rendered with dangerouslySetInnerHTML).
variantAlertDialogAction variant"default"Visual style of the confirm button.
cancelButtonTitlestring"Cancel"Label for the cancel button.
confirmButtonTitlestring"OK"Label for the confirm button.
childrenReact.ReactNodeExtra content between the description block and the footer.
displayError"none" | "above-content" | "below-content""below-content"Where to show an error when action rejects. "none" hides inline error UI (the error is still captured).
renderError(error: Error) => React.ReactNodeCustom error UI. If omitted, a default destructive-styled message with an icon is used.
action(abortSignal: AbortSignal) => void | Promise<void>Runs when the user confirms. Receives a fresh AbortSignal per run; Cancel aborts it so you can pass it to fetch(..., { signal }), streams, or your own cancellation logic. Shows a loading spinner; on failure, rejection is shown per displayError (abort after cancel does not surface as inline error).
disableCancelWhilePendingbooleanIf true, disables the cancel button while the async action is in progress (user cannot abort from the UI until loading finishes or errors).

Usage

Place <Confirmer /> once in your root layout (e.g. Next.js app/layout.tsx, TanStack Start root, or the top-level route that wraps the whole app). It must stay mounted so confirm() can open the dialog from anywhere.

// app/layout.tsx (or equivalent root layout)
import { Confirmer } from "@/components/ui/confirmer"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Confirmer />
      </body>
    </html>
  )
}

Then use confirm() from any component.

Return boolean

Await confirm() to get a boolean. Use when you need to branch based on user choice:

const confirmed = await confirm({
  title: "Close document",
  description: "Your changes will not be saved.",
  variant: "default",
  confirmButtonTitle: "OK",
})

if (confirmed) {
  // do something
}

Action with loading and error

Pass an action to run async work. The dialog shows a loading spinner and can display errors inline:

void confirm({
  title: "Delete Post",
  description: "This action cannot be reversed. Are you sure you want to proceed?",
  variant: "destructive",
  confirmButtonTitle: "Delete post",
  displayError: "below-content",
  action: async (abortSignal) => {
    const result = await deleteCurrentPost({ signal: abortSignal })
    if (result.data) {
      router.replace("/")
    } else {
      throw new Error(result.error)
    }
  },
})

Cancellation and abortSignal

When the user confirms, the confirmer creates a new AbortController and passes controller.signal to action as abortSignal. Pressing Cancel while the action is running calls abort() on that controller, then closes the dialog. Use abortSignal with network calls (fetch(..., { signal })), or listen with abortSignal.addEventListener("abort", …) so in-flight work stops when the user backs out.

If the action ignores abortSignal, the dialog still closes on cancel; the confirmer will not treat the eventual completion as success when the signal was aborted.

Set disableCancelWhilePending: true if you need to block cancellation (and thus abortSignal) until the action settles.

Examples

Return boolean

Action with loading