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…

"use client"

import * as React from "react"
import { Check, ChevronRight } from "lucide-react"
import { cn } from "@/lib/utils"

const CONFIRM_THRESHOLD = 0.9
const KEY_STEP = 0.1

export interface SlideToConfirmProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
  /** Fires exactly once, when the thumb is released past the threshold (or reaches 100% via keyboard). */
  onConfirm: () => void
  /** Track label, fades out as the thumb progresses. */

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/slide-to-confirm.json

Prompt

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 capturesetPointerCapture routes 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-motion it returns instantly while every behavior stays intact.

On This Page