Skip to content
Tier

Confirm button

Pro

A button that asks for a second press before a step that is hard to take back: "Remove item", then "Press again to confirm". The first press arms it (said aloud); a second, separate press within armedFor (long enough to hear that and press again, unless the app says otherwise) runs onConfirm (the second click of a double-click, or Enter held down, doesn't); Escape, moving focus away or the time running out disarms it, and says so. The done label shows only once onConfirm's promise resolves; if it rejects, the reason is shown and said, and the button disarms. It is one element throughout, so focus stays on it.

undo says what can and can't be undone, in the app's words; it is shown under the button and is its description in every state. For a step that can't be taken back at all, pair it with HoldToConfirm or TypeToConfirm instead.

form says how the label changes: confirm (the default) crossfades it, roll rolls the next words up, slide slides the old label off, and morph also turns the armed button into the destructive fill.

Markdown
Version 5.2.0· other versions

This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).

Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 3.0.0, 4.0.0, 4.0.1, 4.0.2, 4.1.0, 4.2.0 and 4.3.0: the page came later, or had another name then.

Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.

Preview

Loading the preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/confirm-button

Brings in: bidi, button, locale, locked, motion, tokens, use-announcer, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import { ConfirmButton } from "@/components/candor/ui/confirm-button"
import { Button } from "@/components/candor/ui/button"

/*
 * The removals here stand in for your own service: the done label waits for its answer. The second one is refused,
 * and the button shows why. For a step that can't be taken back at all, use HoldToConfirm or TypeToConfirm instead.
 */
const remove = (ok: boolean) => new Promise<void>((resolve, reject) => setTimeout(() => (ok ? resolve() : reject(new Error("They still own 2 running agents. Hand those over first."))), 700))

export default function Example() {
  const [run, setRun] = React.useState(0)
  return (
    <div className="flex flex-col gap-6">
      <ConfirmButton key={`a-${run}`} label="Remove item" doneLabel="Removed" undo="You can restore it from Trash for 30 days." onConfirm={() => remove(true)} />
      <ConfirmButton
        key={`b-${run}`}
        form="roll"
        variant="outline"
        label="Remove member"
        confirmLabel="Press again to remove"
        doneLabel="Removed"
        undo="You can invite them again; their past work stays."
        onConfirm={() => remove(false)}
      />
      <Button variant="outline" size="sm" className="self-start" onClick={() => setRun((n) => n + 1)}>
        Reset the examples
      </Button>
    </div>
  )
}

<ConfirmButton>

PropTypeDefaultDescription
labelrequiredstring—The step, as a verb: "Remove item", "Revoke key".
confirmLabelstring | undefined—The label once armed. Defaults to "Press again to confirm".
doneLabelstring | undefined—The label once `onConfirm` has resolved: "Removed". Defaults to "Done".
undorequiredstring—What can and can't be undone, in the app's words: "You can restore it from Trash for 30 days." Shown and read with the button.
onConfirmrequired() => Promise<void> | void—Runs the step on the second press. The done label waits for its promise; a rejection's message is shown as the reason.
form"confirm" | "roll" | "slide" | "morph" | undefined"confirm"How the label changes: `confirm` (the default), `roll`, `slide` or `morph`.
armedFornumber | undefined—How long it stays armed after the first press, in milliseconds. Left out, it is long enough to hear the armed announcement at a slow screen-reader rate and then press again, in whole seconds and never under 4 (6 seconds for the English words); the announcement says how long. Set, it is the app's: give at least as long.
variant"default" | "destructive" | "outline" | "secondary" | "ghost" | undefined"destructive"The button's look: `destructive` (the default) for removal, or another of Button's variants.
size"default" | "xs" | "sm" | "lg" | undefined—The button's size, as Button's.
lockedstring | null | undefined—Why the viewer can't take this step. The button stays focusable, says why, and doesn't arm. `null` or left out: unlocked.
classNamestring | undefined—Classes for the wrapper round the button and its words.

Strings

The fixed words, in English by default. Change any of them under confirmButton in <LocaleProvider strings={{ … }}>; the table is exported as confirmButtonStrings.

KeyEnglish
confirmPress again to confirm
doneDone
disarmedNot confirmed. Nothing was done.
failedCouldn't do it: {reason}
failedNoReasonCouldn't do it. No reason was given.
armed{confirm}. It disarms in {seconds}.

Motion

MomentFamilyWhat it does
donemark, drawThe tick is drawn along its path when `onConfirm`'s promise resolves, beside the done label.
labelvalue, fadeWith `form="confirm"` or `"morph"`, the label crossfades to the next words when the person arms the button, when it disarms and when the step is done. The label it is drawn with is shown still.
rollvalue, slideWith `form="roll"`, the next words roll up into place as the old ones go, when the person arms the button, when it disarms and when the step is done. Under reduced motion they crossfade.
slidemoveWith `form="slide"`, the old label slides off and the next one slides into its place, when the person arms the button, when it disarms and when the step is done. Under reduced motion the label is simply replaced.
workingliveWhile `onConfirm`'s promise is pending, a spinner turns beside the label. Under reduced motion it is still, with its words.

Set with --cd-motion-confirm-button-<moment>-* in your brand file; the families are in the design reference.