Slide to Confirm
A track you swipe the knob across to confirm; the label fades as you go, short drags spring back, completion locks.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/slide-to-confirm.json'use client'
import { useEffect, useRef, useState } from 'react'
import './slide-to-confirm.css'
export interface SlideToConfirmProps {
label?: string
doneLabel?: string
onConfirm?: () => void
resetAfter?: number | null
fill?: 'dark' | 'light'
className?: string
}
/** Drag-to-confirm track. */
export function SlideToConfirm({ label = 'Slide to pay $48.00', doneLabel = 'Paid', onConfirm, resetAfter = 2200, fill = 'dark', className }: SlideToConfirmProps) {
const [x, setX] = useState(0)
const [drag, setDrag] = useState(false)
const [done, setDone] = useState(false)
const root = useRef<HTMLDivElement>(null)
const start = useRef(0)
const max = () => (root.current ? root.current.clientWidth - 60 : 240)
useEffect(() => {
if (!done || resetAfter === null) return
const t = setTimeout(() => { setDone(false); setX(0) }, resetAfter)
return () => clearTimeout(t)
}, [done, resetAfter])
const finish = () => { setX(max()); setDone(true); onConfirm?.() }
const p = x / (max() || 1)
return (
<div ref={root} className={['sd-stc', className].filter(Boolean).join(' ')} data-fill={fill} data-drag={drag || undefined} data-done={done || undefined}>
<span className="sd-stc__fill" style={{ width: done ? '100%' : x + 55 }} aria-hidden />
<span className="sd-stc__lab" style={{ opacity: done ? 1 : Math.max(0, 1 - p * 1.6) }} aria-hidden>{done ? doneLabel : label}</span>
<button
type="button"
className="sd-stc__knob"
aria-label={done ? doneLabel : label.replace('Slide', 'Press Enter')}
disabled={done}
style={{ transform: `translateX(${x}px)` }}
onPointerDown={(e) => { if (done) return; e.currentTarget.setPointerCapture(e.pointerId); start.current = e.clientX - x; setDrag(true) }}
onPointerMove={(e) => drag && setX(Math.max(0, Math.min(max(), e.clientX - start.current)))}
onPointerUp={() => { if (!drag) return; setDrag(false); if (x > max() * 0.92) finish(); else setX(0) }}
onKeyDown={(e) => { if ((e.key === 'Enter' || e.key === ' ') && !done) { e.preventDefault(); finish() } }}
>
<svg viewBox="0 0 18 18" aria-hidden>{done ? <path d="m4 9.5 3.5 3.5L14 6" /> : <path d="M7 4.5 11.5 9 7 13.5" />}</svg>
</button>
<span className="sr" aria-live="polite">{done ? doneLabel : ''}</span>
</div>
)
}.sd-stc { --bg: #1c1c1c; --fg: #f6f6f6; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.1); --soft: rgb(255 255 255 / 0.07); --strong: #fafafa; --on: #111;
color: var(--fg); font-size: 14px; line-height: 1.4; -webkit-tap-highlight-color: transparent; }
.sd-stc[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.09); --soft: rgb(0 0 0 / 0.05); --strong: #111; --on: #fff; }
.sd-stc button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-stc :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-stc .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-stc { position: relative; width: min(100%, 300px); height: 60px; padding: 5px; border-radius: 99px; background: var(--soft); box-shadow: inset 0 0 0 1px var(--line); user-select: none; overflow: hidden; }
.sd-stc__fill { position: absolute; left: 0; top: 0; bottom: 0; border-radius: 99px; background: var(--strong); }
.sd-stc__lab { position: absolute; inset: 0; display: grid; place-items: center; padding-left: 40px; font-size: 14.5px; font-weight: 600; color: var(--mute); pointer-events: none; }
.sd-stc[data-done] .sd-stc__lab { color: var(--on); padding-left: 0; animation: sd-stc-in 300ms ease; }
@keyframes sd-stc-in { from { opacity: 0; } }
.sd-stc__knob { position: relative; z-index: 1; display: grid; place-items: center; width: 50px; height: 50px; border-radius: 99px; background: var(--fg) !important; color: var(--bg) !important; touch-action: none; cursor: grab; box-shadow: 0 2px 8px rgb(0 0 0 / 0.3); }
.sd-stc[data-drag] .sd-stc__knob { cursor: grabbing; }
.sd-stc:not([data-drag]) .sd-stc__knob, .sd-stc:not([data-drag]) .sd-stc__fill { transition: transform 420ms cubic-bezier(0.3, 1.5, 0.4, 1), width 420ms cubic-bezier(0.3, 1.5, 0.4, 1); }
.sd-stc[data-done] .sd-stc__knob { background: var(--on) !important; color: var(--strong) !important; }
.sd-stc__knob svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
@media (prefers-reduced-motion: reduce) { .sd-stc *, .sd-stc *::before, .sd-stc *::after { animation: none !important; transition: none !important; } }Overview
Drag the knob to the end of the track to confirm. The label fades with distance so the gesture explains itself; let go early and the knob springs home.
At the end the knob becomes a tick and the track fills. Keyboard users press Enter on the knob instead, since sliding is an enhancement, not a gate.
When to use
- Payments, irreversible sends and unlocks on touch screens.
When not to use
- Routine actions where a plain button is faster.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/slide-to-confirm.json$
npx shadcn@latest add https://sundaeui.dev/r/slide-to-confirm.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/slide-to-confirm.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/slide-to-confirm.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy slide-to-confirm.tsx and slide-to-confirm.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { SlideToConfirm } from '@/components/sundae/slide-to-confirm'
<SlideToConfirm />Props
| Prop | Type | Default |
|---|---|---|
labelTrack label. | string | 'Slide to pay' |
doneLabelLabel when confirmed. | string | 'Paid' |
onConfirmCalled at the end. | () => void | — |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- The knob is a button: Enter or Space confirms, so the gesture is never required.
- Completion is announced.
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-stc; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
Keyboard
- Enter / Space (on knob)
- Confirm
FAQ
- Is Slide to Confirm free?
- Yes. Slide to Confirm is free for personal and commercial projects, with full source on this page.
- How do I install Slide to Confirm?
- Run npx shadcn@latest add https://sundaeui.dev/r/slide-to-confirm.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Slide to Confirm support keyboard and screen readers?
- The knob is a button: Enter or Space confirms, so the gesture is never required. Completion is announced.
- Does Slide to Confirm respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All components →- Pro
Swipe Actions
Components · Refined - Pro
Sheet Drawer
Components · Refined Hold to Confirm
Components · RefinedFollow Button
Components · RefinedDownload Button
Components · RefinedBookmark Button
Components · Refined