Hold to confirm
ProA 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.
Preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/hold-to-confirm
Usage
"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>
| Prop | Type | Default | Description |
|---|---|---|---|
| labelrequired | string | — | The step, as a verb: "Delete 3 rows", "Send to 240 people". |
| reasonrequired | React.ReactNode | — | What the step does and whether it can be undone, in the app's words. Shown under the button and read with it. |
| holdMs | number | undefined | 1200 | How long the hold lasts, in milliseconds. 1200 unless the app says otherwise. |
| onConfirmrequired | () => void | — | Called once, when the hold has lasted `holdMs`. |
| locked | string | 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. |
| className | string | 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.
| Key | English |
|---|---|
| hold | Press and hold for {duration}. Letting go early cancels. |
| progress | Holding |
| cancelled | Cancelled: let go before the hold finished. Nothing was done. |
| confirmed | Confirmed: {label}. |