Mischief

03 / Controls

Hold Button

A confirmation button for actions that deserve a second thought. Release early to cancel, or activate once with a keyboard.

Release early to cancel.

Installation

Copy the source into your project, or keep it behind a package.

npx shadcn@latest add Tinkerers-Labs/mischief-ui/hold-button
import { HoldButton } from "mischief-ui/hold-button"

Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.

registry/default/hold-button/hold-button.tsx
"use client" import * as React from "react"import { cn } from "@/lib/utils" type HoldState = "idle" | "holding" | "complete" export interface HoldButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {  onComplete: () => void  duration?: number  completeLabel?: React.ReactNode} export const HoldButton = React.forwardRef<HTMLButtonElement, HoldButtonProps>(

Usage

export function RemoveDownload() {
  return (
    <HoldButton onComplete={removeDownload}>
      Hold to remove download
    </HoldButton>
  )
}

Why hold instead of confirm

A confirmation dialog asks a question the answer to which is almost always yes, and people learn to dismiss it without reading. A hold cannot be dismissed by reflex: it takes a second of deliberate, continuous pressure, and letting go early cancels it.

That makes it a good fit for the destructive action that is common enough to be annoying behind a dialog but severe enough that an accident matters -- deleting a draft, clearing a queue, revoking a key. It is a poor fit for anything irreversible and rare, where a dialog that names what is about to happen is still the right answer.

How long the hold is

The default is 900ms, which is long enough to feel like a decision and short enough not to feel broken. Shorter values are accepted but floored at 500ms, because below that the hold stops being deliberate and becomes a slow click -- exactly the reflex it exists to interrupt.

onComplete runs once, at the end of a full hold. Releasing early, dragging off the button, or pressing Escape all cancel it, and nothing is reported.

API

onComplete() => voidRuns once after a completed hold or keyboard activation.
durationnumberHold time in milliseconds. Defaults to 900, minimum 500.
completeLabelReactNodeContent shown after completion.
childrenReactNodeThe idle button content.
...buttonPropsButtonHTMLAttributesNative button attributes except pointer and click handlers.

Accessibility

Pointer users hold to confirm. Keyboard and assistive technology users activate the native button once, avoiding a timing barrier. Progress and completion are announced politely.