Components / Buttons
Slide To Confirm Button
Free Buttons React component — SlideToConfirmButton. MIT licensed, copy-paste ready for Next.js and Tailwind CSS.
Drag the knob across the track to commit an action — snaps back if you release early, locks green when confirmed. Great for irreversible or high-intent actions.
Preview
Setup
How to use
Free for personal and commercial use. No UI attribution required. Include the MIT copyright notice when copying component source into your project. Read the MIT license.
- 1
Run in your terminal:
$npm install clsx tailwind-merge lucide-react - 2
Copy
lib/cn.tsbelow. Skip if you already havecn(). - 3
Copy the code below and create
components/buttons/slide-to-confirm-button.tsxin your project. - 4
Import and render:
Exampleimport { SlideToConfirmButton } from "@/components/buttons/slide-to-confirm-button";
lib/cn.ts
import { type ClassValue, clsx } from "clsx";import { twMerge } from "tailwind-merge";export function cn(...inputs: ClassValue[]) {return twMerge(clsx(inputs));}
components/buttons/slide-to-confirm-button.tsx
"use client";import {forwardRef,useRef,useState,type ComponentPropsWithoutRef,type PointerEvent as ReactPointerEvent,} from "react";import { cn } from "@/lib/cn";import { ArrowRight, Check } from "lucide-react";const PAD = 4;const KNOB = 48;export type SlideToConfirmButtonProps = Readonly<{label?: string;confirmedLabel?: string;onConfirm?: () => void;} & Omit<ComponentPropsWithoutRef<"div">, "onConfirm">>;// Slide-to-confirm — drag the knob across the track to commit an action.export const SlideToConfirmButton = forwardRef<HTMLDivElement,SlideToConfirmButtonProps>(({className,label = "Slide to confirm",confirmedLabel = "Confirmed",onConfirm,...props},ref,) => {const trackRef = useRef<HTMLDivElement>(null);const draggingRef = useRef(false);const [x, setX] = useState(0);const [confirmed, setConfirmed] = useState(false);const maxX = () => {const track = trackRef.current;if (!track) return 0;return track.offsetWidth - KNOB - PAD * 2;};const handleDown = (event: ReactPointerEvent<HTMLButtonElement>) => {if (confirmed) return;draggingRef.current = true;event.currentTarget.setPointerCapture(event.pointerId);};const handleMove = (event: ReactPointerEvent<HTMLButtonElement>) => {if (!draggingRef.current || confirmed) return;const track = trackRef.current;if (!track) return;const rect = track.getBoundingClientRect();const next = Math.min(Math.max(event.clientX - rect.left - PAD - KNOB / 2, 0),maxX(),);setX(next);};const handleUp = () => {if (!draggingRef.current) return;draggingRef.current = false;if (x >= maxX() - 4) {setX(maxX());setConfirmed(true);onConfirm?.();} else {setX(0);}};const progress = maxX() > 0 ? x / maxX() : 0;return (<divref={ref}data-slot="slide-to-confirm-button"className="font-sans select-none"{...props}><divref={trackRef}className={cn("relative h-14 w-72 overflow-hidden rounded-full p-1 transition-colors duration-300",confirmed ? "bg-emerald-500" : "bg-neutral-100",className,)}><spanaria-hiddenclassName={cn("absolute inset-0 flex items-center justify-center text-sm font-medium transition-colors duration-200",confirmed ? "text-white" : "text-neutral-400",)}style={{ opacity: confirmed ? 1 : 1 - progress * 1.4 }}>{confirmed ? confirmedLabel : label}</span><buttontype="button"aria-label={label}disabled={confirmed}onPointerDown={handleDown}onPointerMove={handleMove}onPointerUp={handleUp}onPointerCancel={handleUp}className={cn("absolute top-1 left-1 flex size-12 touch-none items-center justify-center rounded-full bg-white text-neutral-700 shadow-sm",confirmed ? "cursor-default" : "cursor-grab active:cursor-grabbing",draggingRef.current? "": "transition-transform duration-300 ease-out",)}style={{ transform: `translateX(${x}px)` }}>{confirmed ? (<Check size={18} strokeWidth={2.5} className="text-emerald-500" />) : (<ArrowRight size={18} strokeWidth={2.5} />)}</button></div></div>);},);SlideToConfirmButton.displayName = "SlideToConfirmButton";