Hold to Confirm

A button that fires only after a full press-and-hold, so a stray click or tap doesn't trigger a destructive action.

StopsHelps prevent a destructive action fired by a stray click

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/hold-to-confirm.json

The files land in your project. They're yours to edit.

Usage

hold-to-confirm-usage.tsx
import { HoldToConfirm } from "@/components/ui/hold-to-confirm"

<HoldToConfirm
  variant="destructive"
  pendingLabel="Deleting…"
  confirmedLabel="Deleted"
  onConfirm={() =>
    new Promise<void>((resolve) =>
      setTimeout(() => {
        setLog("Workspace deleted.")
        resolve()
      }, 700),
    )
  }
>
  <Trash2 aria-hidden className="size-4" /> Delete workspace
</HoldToConfirm>

What it catches

  • Stray click or tap on a delete or other irreversible button
  • Releasing early: progress rewinds
  • Plain click: a hint explains to hold
  • Users who can't hold: a second press confirms instead

Props

HoldToConfirm props
PropTypeDefault
onConfirm* (required)

Fires once the button has been held for the full duration. Return a promise to show a pending state.

() => void | Promise<void>—
duration

How long the user must hold, in milliseconds.

number1200
variant

Visual intent. `destructive` is for delete and other irreversible actions.

keyof typeof variants"default"
size

Button size.

keyof typeof sizes"default"
holdHint

Shown briefly when someone clicks instead of holding, and read to screen readers.

string"Hold to confirm"
pendingLabel

Label while an async `onConfirm` is running.

React.ReactNode"Working…"
confirmedLabel

Label shown for a moment after the action is confirmed.

React.ReactNode"Done"
resetAfter

Milliseconds before the button resets after confirming. 0 keeps it confirmed.

number1600
confirmAgainLabel

Screen readers and switch access send a plain click and can't hold. Such a click arms the button and a second one within a few seconds confirms, so the safeguard stays without locking anyone out.

React.ReactNode"Press again"

* required. Also accepts the native props of <button> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Native button; Space or Enter can be held from the keyboard
  • Hint text is linked via aria-describedby and status changes use role=status
  • aria-busy while onConfirm is pending; icons are aria-hidden
  • Fill and label animations are reduced under reduced motion

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

This is a UX guard rail. Enforce on the server: Authorize the destructive action on the server and make it idempotent.

  • Friction, not authorization: anyone who holds it confirms
  • The second-press path is quicker than holding, by design
  • Doesn't name the target; pair it with clear wording or a readback

What it stops

Helps prevent a destructive action fired by a stray click

Also known as

long press · press and hold · hold button · delete button · confirm button · mobile tap · destructive action

Dependencies

Pairs well with