Buttons & Toggles

Hold to Confirm

A press-and-hold fill confirmation button: holding fills the progress from left to right with a slight tremor near completion, and only a full fill triggers it — preventing accidental taps on dangerous actions.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add hold-to-confirm.en

New project or installation trouble? Start here →

Hold to Confirm is a React hold to confirm button built with Tailwind CSS and Motion. Users have to keep holding the mouse button, a finger or the Space key until the fill completes, which makes it a lighter alternative to an "Are you sure?" dialog.

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

1.2
0
Active edited usageDefault props
<HoldToConfirm/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

When to use

  • Irreversible actions such as deleting an account, deleting a project or wiping a database.
  • Canceling a subscription or stopping a running deployment or batch job.
  • Admin actions that affect other people, like removing a member or revoking API access.
  • Mobile buttons that need protection from accidental taps without adding a dialog.
  • Not a good fit: regular buttons that show loading, success and error after submit, use Stateful Button; for a drag gesture instead, see Slide to Unlock.

Installation

npx webberui@latest add https://webberui.com/r/hold-to-confirm.en.json

Installing also writes the --wb-duration-fast CSS variable into your global stylesheet.

Usage

import { HoldToConfirm } from "@/components/ui/hold-to-confirm";

<HoldToConfirm
  onConfirm={() => deleteAccount()}
  confirmedChildren="Deleted"
>
  Hold to delete account
</HoldToConfirm>

Examples

Call an API and reset on failure

After the fill completes the component stays in its success state. onConfirm is a synchronous callback, so start the request inside it and change the key on failure to remount the button in its initial state.

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

export function DeleteProjectButton({ projectId }: { projectId: string }) {
  const [attempt, setAttempt] = React.useState(0);
  const [error, setError] = React.useState<string | null>(null);

  return (
    <div className="flex flex-col items-start gap-2">
      <HoldToConfirm
        key={attempt}
        holdDuration={1.5}
        confirmedChildren="Project deleted"
        onConfirm={async () => {
          setError(null);
          const res = await fetch(`/api/projects/${projectId}`, { method: "DELETE" });
          if (!res.ok) {
            setError("Could not delete the project. Try again later.");
            setAttempt((n) => n + 1);
          }
        }}
      >
        Hold to delete project
      </HoldToConfirm>
      {error && <p className="text-sm text-red-600">{error}</p>}
    </div>
  );
}

Neutral variant

variant="default" uses neutral colors for important but non-destructive actions, such as publishing or archiving.

<HoldToConfirm variant="default" holdDuration={0.8} onConfirm={() => publishRelease()}>
  Hold to publish v2.4.0
</HoldToConfirm>

With an undo snackbar: rollback, tolerance and instant keyboard confirm

This combination makes holding feel smoother: an early release rolls back with a 180ms ease-out, a finger can drift up to 8px outside the button without canceling, keyboard users confirm right away with Enter or Space, and the button resets itself 1.8 seconds after success. onConfirm receives "keyboard" or "pointer", which you can log or use to tweak hint text. Pair it with Undo Snackbar for a 5-second undo window so dangerous actions still have a way back.

import { HoldToConfirm } from "@/components/ui/hold-to-confirm";
import { UndoSnackbar, useUndoSnackbar } from "@/components/ui/undo-snackbar";

export function DeleteWithUndo() {
  const { show, snackbarProps } = useUndoSnackbar({ duration: 5000 });

  return (
    <>
      <HoldToConfirm
        rollbackDuration={180}
        leaveTolerance={8}
        instantKeyboard
        instantReducedMotion
        resetAfter={1800}
        confirmedChildren="Deleted"
        onConfirm={(source) => {
          softDeleteProject();
          show("Deleted 1 project", {
            onUndo: () => restoreProject(),
            onCommit: () => purgeProject(),
          });
          console.log("Confirmed via:", source);
        }}
      >
        Hold to delete project
      </HoldToConfirm>
      <UndoSnackbar {...snackbarProps} />
    </>
  );
}

All of these new props are off or unset by default; leave them out and you keep the original spring retreat, cancel-on-leave and stay-confirmed behavior.

Disabled state and custom styles

Other button attributes pass through, and className merges with the defaults through cn, so you can override the width or radius.

<HoldToConfirm
  disabled={!canDelete}
  aria-label="Hold to remove member"
  className="w-full rounded-full"
  onConfirm={() => removeMember(memberId)}
>
  Remove member
</HoldToConfirm>

Props

PropTypeDefaultDescription
holdDurationnumber1.2How long (seconds) it must be held to confirm; the fill completes at a constant rate over this duration
onConfirm(source: 'keyboard' | 'pointer') => void—Fires when the fill completes, exactly once per success; receives the source
confirmedChildrenReactNode—Content shown after a successful confirmation; falls back to children when not provided
variant'danger' | 'default''danger'danger is the red scheme (for dangerous actions), default is neutral
rollbackDurationnumber—On an early release, roll back to 0 with an ease-out over this many ms; unset keeps the spring retreat. Suggested 180
leaveTolerancenumber—Cancel only once the pointer moves this many px outside the button; unset cancels as soon as it leaves. Suggested 8
instantKeyboardbooleanfalsePressing Enter / Space fills instantly and confirms, no holding needed
instantReducedMotionbooleanfalseWith "reduce motion" on, a pointer press fills instantly and confirms
resetAfternumber—Return to the initial state this many ms after confirming; unset stays confirmed. Suggested 1800
classNamestring—Overrides the default styles
childrenReactNode—Button content

All other <button> attributes (disabled, aria-*, and so on) are forwarded directly. A click never confirms — holding is the only confirmation gesture.

How it works

  • Constant-rate fill: on pointer down (or holding the space bar) the fill layer sweeps from left to right with scaleX at a linear rate; if you let go early it stops midway, and pressing again resumes from the current progress at the same rate
  • Tremor near the threshold: past 65% the button starts shaking slightly from side to side, with the amplitude growing with progress to build tension for "about to take effect"
  • Spring retreat: on an early release the fill springs back to 0 quickly (stiffness: 500, damping: 42); with rollbackDuration it eases out over that duration instead
  • Leave tolerance: with leaveTolerance the pointer is captured, so a finger drifting slightly off the button keeps filling and only moving past the distance cancels
  • Auto reset: with resetAfter the success state returns to the initial state after that many milliseconds, no key change needed
  • Success state: a full fill triggers onConfirm, the button turns green, a checkmark draws itself in, and confirmedChildren is shown. The component stays in the success state afterwards; to replay it, change the key to remount (see the demo)
  • Keyboard support: space bar keydown starts the fill and keyup cancels it, matching the mouse press-and-hold behavior; e.repeat is filtered out so holding it down does not fire repeatedly. With instantKeyboard, Enter and Space fill instantly and confirm, and onConfirm receives "keyboard"
  • Accessibility: a "hold to confirm" description is attached with aria-describedby, and success is announced through aria-live="polite"
  • When the user has "reduce motion" enabled at the system level: the hold duration and the linear fill are kept exactly as they are (functionality unchanged), and only the tremor is removed, with the retreat becoming a fast linear one; with instantReducedMotion a pointer press fills instantly and confirms
  • touch-none + select-none prevent a touch hold from triggering scrolling or text selection; the pointer leaving the button, the event being cancelled (pointercancel), or losing focus during a keyboard hold all count as a release and cancel it

FAQ

Does a single click trigger it?

Not by default. Clicks and Enter never confirm (with instantKeyboard, Enter / Space confirm right away). Users must hold the left mouse button, a finger or the Space key for holdDuration seconds (default 1.2) before onConfirm runs; releasing early springs the fill back to 0.

How do I return it to the initial state after confirming?

Set resetAfter (for example 1800) to reset automatically after success. Without it the button stays in its green success state, and you can change its key to remount it.

Does it work with the Next.js App Router?

Yes. The source starts with "use client", so it is a client component you can import from a Server Component page; onConfirm can call a Server Action.

How does it work with keyboards and screen readers?

Focus it and hold Space to fill; releasing or losing focus cancels. The button is linked through aria-describedby to a hidden hint explaining that it must be held, and announces confirmation through aria-live="polite" on success.

What happens with reduced motion?

The hold duration and linear fill stay the same; only the shake near completion is removed, and an early release returns with a quick linear animation instead of a spring. With instantReducedMotion, a press fills instantly and confirms, so pair it with Undo Snackbar to make accidental presses recoverable.