Hold Button
Press and hold to confirm, for the actions a stray tap must never set off: a panic call, ending a call for everyone, wiping a device. The fill is a timed Progress running on the compositor, and letting go early cancels with a shake.
Installation#
Usage#
import { HoldButton } from "@/components/ui/hold-button"
export function Example() {
return (
<HoldButton variant="destructive" holdMs={1000} onConfirm={() => raisePanic()}>
Hold to raise panic
</HoldButton>
)
}Variants#
neutral (the default) sits on the secondary surface and fills with an ink tint. destructive is for panic-like actions: Button's soft red at rest, a deeper tint sweeping in while held, and the solid red once it fires. Both turn solid on confirm and stay that way until released, so the hold that did it is unmistakable.
Sizes#
md (the default) is 40px, the hit target on its own; lg is 48px, for the one action a panel is about. Give it className="w-full" to span a row.
Hold time and callbacks#
holdMs sets how long the hold must last (1000ms by default). onConfirm fires the moment it has, while the button is still held; a release before that fires onCancel(ms) with the time held, and the control shakes. Sliding the pointer off the button, tabbing away and the window losing focus all end the hold the same way. Here the hold is two seconds.
Keys#
keys picks which keys hold the focused button, as KeyboardEvent.key names ("Space" is accepted for " "). Space and Enter by default; ["Enter"] leaves Space to a surrounding list that uses it, and [] turns the keyboard hold off for a parent that drives it.
<HoldButton keys={["Enter"]} onConfirm={() => endCall()}>
Hold Enter to end the call
</HoldButton>Controlled#
A parent that owns the keyboard (a tablet whose list runs on arrow keys, a game binding) drives the hold with holding and onHoldingChange, the same pair as a dialog's open. true starts a hold and false ends it, confirmed or cancelled exactly as a release. Controlled, the button's own gestures (pointer, keys, sliding off, a lost window) only ask through onHoldingChange and wait for the prop; disabling it still ends the hold. onHoldStart fires whenever a hold begins, in both modes. Here the page listens for H itself.
Disabled#
disabled dims the control, and disabling it mid-hold ends the hold without confirming. A one-shot action can disable itself from onConfirm.
Accessibility#
It is a native <button>: Space or Enter held on it is the same hold as a pointer, and Enter never clicks it through. The button is described by a hint, "Press and hold for 1 second" by default, phrased from holdMs; pass hint to translate it. Your own aria-describedby is kept after it. The fill is a timed Progress hidden from assistive tech, and under reduced motion it ticks instead of gliding. Some screen readers activate a button with a single synthetic click, which can never be a hold: for an action people must be able to reach that way, offer a second path too (a menu item with a confirm dialog).
API reference#
HoldButton
A <button> with data-state="idle" | "holding" | "confirmed". Props: holdMs (default 1000), onConfirm(), onCancel(heldMs), onHoldStart(), holding and onHoldingChange(holding) (controlled hold), keys (default [" ", "Enter"]), hint, variant (neutral · destructive), size (md · lg) and disabled. Children are the label (an icon is sized like Button's). Every button prop is forwarded; className is merged last. The fill is a Progress inside it, reachable as [data-slot=progress-indicator].