Hold to confirm

Pro

A button a person holds down before a risky step runs: the pointer, or Space or Enter, kept down for holdMs (1.2 seconds unless the app says otherwise). The bar under it fills with the real time held, from the clock, and letting go early cancels and says so. Nothing is armed in advance, no single key fires it, and it's never focused for the person. If locked is set while a hold is under way, the hold is cancelled and onConfirm is not called; onConfirm and holdMs are read when the hold ends, so the latest of each applies. Say what the step does and whether it can be undone in reason; it's read with the button.

Some people can't hold a key or a pointer down. Offer them another way to take the same step (TypeToConfirm, or a plain confirmation) rather than making this the only path.

Markdown

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/hold-to-confirm

Brings in: button, locale, locked, utils

Usage

TSX · The example above
"use client"

import * as React from "react"
import { HoldToConfirm } from "@/components/hold-to-confirm"
import { Button } from "@/components/ui/button"

export default function Example() {
  const [done, setDone] = React.useState(false)
  if (done)
    return (
      <div className="flex flex-col items-start gap-2 text-sm">
        <p>The export is being sent to 240 people.</p>
        <Button variant="outline" size="sm" onClick={() => setDone(false)}>
          Reset the example
        </Button>
      </div>
    )
  return (
    <div className="flex flex-col gap-6">
      <HoldToConfirm
        className="max-w-sm"
        label="Send to 240 people"
        reason="Sends the export to every member of the Sales team. Sent email can’t be recalled."
        onConfirm={() => setDone(true)}
      />
      <HoldToConfirm
        className="max-w-sm"
        variant="destructive"
        label="Delete the staging database"
        reason="Removes staging-eu’s database and its 14 scheduled jobs. It can be restored from last night’s backup, not from today’s."
        locked="Only an admin can delete a database."
        onConfirm={() => {}}
      />
    </div>
  )
}

<HoldToConfirm>

PropTypeDefaultDescription
labelrequiredstring—The step, as a verb: "Delete 3 rows", "Send to 240 people".
reasonrequiredReact.ReactNode—What the step does and whether it can be undone, in the app's words. Shown under the button and read with it.
holdMsnumber | undefined1200How long the hold lasts, in milliseconds. 1200 unless the app says otherwise.
onConfirmrequired() => void—Called once, when the hold has lasted `holdMs`.
lockedstring | undefined—Why the viewer can't take this step at all. The button stays visible and says so, and no hold starts.
variant"default" | "destructive" | undefined"default""destructive" for removal, "default" for a serious change that isn't one.
classNamestring | undefined—Extra classes for the outer element.

Strings

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

KeyEnglish
holdPress and hold for {duration}. Letting go early cancels.
progressHolding
cancelledCancelled: let go before the hold finished. Nothing was done.
confirmedConfirmed: {label}.