HoldButton

HoldButton guards destructive actions behind a deliberate press-and-hold gesture. While held, an inner fill wipes across the button from 0% to 100% over a configurable duration. Release before it completes and the button shakes, the fill snaps back to 0%, and the operation is aborted. Hold all the way to 100% and the action fires.

Preview

HoldButton

Press and hold to delete

Variants

Fast hold

A snappier 800ms hold for low-stakes confirmations.

Fast hold

Hold for 800ms

Slow hold

A longer 2.5s hold for high-stakes, irreversible actions.

Slow hold

Hold for 2.5s

Install

Add the item with the shadcn CLI.

npx shadcn@latest add @evilbuttons/hold-button

Usage

[]txt
import { HoldButton } from "@/components/evil-buttons/hold-button";

export function ButtonDemo() {
  return (
    <HoldButton
      duration={1500}
      label="Hold to delete"
      successLabel="Deleted"
      onConfirm={() => deleteAccount()}
      onAbort={(progress) => console.log("aborted at", progress)}
    />
  );
}

Props

The component spreads any <button> HTML attributes except onClick and onAbort.

PropTypeDefaultDescription
durationnumber1500Milliseconds the user must hold before onConfirm fires.
labelReact.ReactNode"Hold to delete"Label shown in the idle state.
holdingLabelReact.ReactNode"Keep holding…"Label shown while the button is being held.
successLabelReact.ReactNode"Deleted"Label shown after the hold completes.
onConfirm() => void-Fired once the fill reaches 100%.
onAbort(progress: number) => void-Fired when released early, with the progress (0-1) reached.
resetAfternumber1400Milliseconds to stay in the success state before resetting to idle. Set to 0 to stay.
classNamestring-Extra classes passed to the button.

Notes

  • Progress is driven by requestAnimationFrame for frame-accurate completion, while the fill width and reset use a CSS transition for a smooth snap-back.
  • The fill reveals a knockout white label sized to the full button width via a ResizeObserver, so the text stays crisp and undistorted as the fill wipes across.
  • Pointer capture is used so releasing outside the button still aborts the hold; onLostPointerCapture and onPointerCancel are handled too.
  • Keyboard users can hold Space or Enter; releasing the key before 100% aborts just like a pointer release.
  • aria-live="polite" and data-state (idle | holding | success) expose the current state for screen readers and styling.

Registry

The registry item includes components/evil-buttons/hold-button.tsx and installs clsx and tailwind-merge as dependencies.