Buttons
Slide to Confirm
A drag-to-the-end confirmation control — a track and thumb that replace a risky one-tap action with a deliberate slide gesture.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/slide-to-confirm.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "SlideToConfirm" component using
lucide-react icons (ChevronRight, Check). No animation library — raw Pointer
Events plus CSS transitions.
Contract
- Export a forwardRef div extending React.HTMLAttributes<HTMLDivElement>
(omit onChange) with: onConfirm: () => void (required, fires exactly once),
label = "Slide to confirm", confirmedLabel = "Confirmed",
tone: "default" | "destructive" = "default", disabled?: boolean, className.
- Progress is internal gesture state, not a controlled prop. After confirming
the control locks; consumers reset by remounting it (key change).
Behavior
- The thumb is the drag handle. On pointerdown: setPointerCapture, measure
travel = track content width minus thumb width; on pointermove map the
clientX delta to progress 0-1 (clamped); mirror the latest progress into a
ref so the release decision never reads stale state.
- On release: progress at or past the 0.9 threshold calls onConfirm once,
snaps the thumb to the end and enters the locked confirmed state (Check
icon + confirmedLabel); below threshold it springs back to 0 with a ~300ms
ease-out transition. pointercancel always springs back. While dragging,
transitions are off so the thumb tracks the finger 1:1.
- Keyboard path (mandatory — never mouse-only): the thumb is a focusable
role="slider" with aria-valuemin=0 / aria-valuemax=100 / aria-valuenow, and
aria-valuetext = confirmedLabel once confirmed. ArrowRight/Up +10%,
ArrowLeft/Down -10% (rounded onto the 10% grid so ten presses land exactly
on 100%), Home returns to 0, End jumps to 100. Reaching 100% triggers the
same confirm path.
- disabled and confirmed both ignore pointer + keyboard input (aria-disabled).
Rendering & styling
- Track: relative h-12 w-64 rounded-full bg-muted p-1 overflow-hidden
select-none. Progress fill: absolute aria-hidden layer, width =
progress * 100%, bg-primary/15 (bg-destructive/15 for the destructive tone).
- Thumb: size-10 rounded-full bg-primary text-primary-foreground (destructive
tone: bg-destructive) travelling inside an inset-1 rail, positioned with
left: p*100% plus translateX(-p*100%) so rendering needs no width
measurement (only the drag math measures once, on pointerdown). touch-none
on the thumb so touch drags are not hijacked by page scrolling.
- Label: centered text-sm text-muted-foreground, fades as progress grows
(opacity = 1 - 2p); the confirmed label re-appears in the tone color.
Decorative layers are aria-hidden — state lives on the slider element.
- Semantic tokens only (bg-muted / bg-primary / text-primary-foreground /
bg-destructive / ring tokens); merge consumer className via cn(); the
snap-back transition is motion-reduce:transition-none so reduced-motion
users get an instant return with full function intact.
Customization levers
- Threshold and step: the 0.9 release threshold and 10% keyboard step are
single constants — raise the threshold for scarier actions.
- Size: h-12/w-64 track and size-10 thumb scale together; keep
thumb = track height minus twice the track padding.
- Tone mapping: tones only swap token classes — add a "success" tone by
mapping fill/thumb/ring onto your success tokens. The destructive thumb
reuses text-primary-foreground for its icon; remap it if your theme's
primary-foreground does not read on destructive.
- Snap-back feel: the 300ms ease-out transition is the only motion knob; swap
in a spring library for overshoot, keeping the reduced-motion escape hatch.
- Reset policy: lock-after-confirm is deliberate (anti double-fire). For
auto-reset flows, remount with a new key from the consumer instead of
adding internal timers.Concepts
- Friction by design — the gesture deliberately costs more than a click; the travel distance itself is the confirmation, so stray taps and pocket touches can never fire the action.
- Pointer capture —
setPointerCaptureroutes every subsequent move/up to the thumb even when the pointer leaves the track, so fast or sloppy drags never drop mid-gesture. - Threshold commit — release position, not click, decides the outcome: past 0.9 commits and snaps to the end; anything less springs back to zero, making "almost" read clearly as "not confirmed".
- Keyboard parity — the thumb is a real ARIA slider (valuemin/max/now), stepped by arrow keys onto a 10% grid; reaching 100% runs the exact same commit path, so the safeguard never becomes a pointer-only feature.
- Lock after confirm — a confirming control that can silently re-arm invites double-fires; the component locks and hands reset back to the consumer as an explicit remount.
- Reduced-motion honesty — only the snap-back easing is decorative, so under
prefers-reduced-motionit returns instantly while every behavior stays intact.