Slide to Confirm — Drag Slider HTML CSS JS Snippet

Slide to Confirm · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Transform-based drag
Handle uses translateX and the fill uses scaleX, so the motion is compositor-smooth and survives Tailwind/React export (which animate transforms, not raw width).
Jump-free grab
down records the pointer-to-handle offset, so grabbing the handle anywhere does not snap it to the cursor — it tracks naturally.
Live-measured bounds
maxPos() reads the track and handle sizes on each use, so the end threshold stays correct even if the layout resizes.
Window-level tracking
Move and release listeners on window keep the drag alive when the pointer leaves the track mid-slide.
Threshold commit
Release within a few pixels of the end triggers complete(); anything short animates back, enforcing a deliberate full slide.
Locked confirmed state
On confirm, a done flag blocks further dragging so the action cannot double-fire, and the track turns green with a check label.
Snap-back animation
An .animate class adds transition: transform only for snap-back and confirm, keeping the active drag perfectly 1:1.
Touch + mouse
Pointer coordinates are read from e.touches or e.clientX, so it works identically on phones and desktops.

About this UI Snippet

Slide to Confirm — Drag-to-Commit Handle, Snap-Back & Confirmed Lock State

Screenshot of the Slide to Confirm snippet rendered live

"Slide to confirm" is the deliberate-action control: instead of a one-tap button that is easy to hit by accident, the user must drag a handle all the way across a track to commit. It is the right pattern for irreversible or high-stakes actions — sending a payment, deleting an account, dispatching an order, unlocking a device. This snippet implements it in plain HTML, CSS, and vanilla JavaScript with full touch and mouse support: a draggable handle, a fill that tracks the drag, snap-back if released early, and a locked confirmed state.

Transform-based dragging (smooth everywhere)

The handle moves with transform: translateX and the green fill grows with transform: scaleX from a left origin — both are compositor-friendly transforms, so the drag is smooth and, importantly, the animation survives a Tailwind/React export unchanged (utility frameworks animate transforms but not raw width). During an active drag the position is set directly with no transition for 1:1 finger tracking; only the snap-back and confirm use a .animate class that adds transition: transform.

Accurate pointer math

On press, down records the offset between the pointer and the handle's current position, so the handle does not jump to the cursor. move then sets the position as pointerX − offset, clamped between 0 and maxPos() (the track width minus the handle and padding, measured live so it stays correct if the layout changes). The move and up listeners live on window, so dragging continues even if the pointer leaves the track.

Threshold, snap-back, and lock

On release, up checks whether the handle reached the end (within a few pixels of maxPos()). If it did, complete() locks the control: it snaps the handle fully right, fills the track green, changes the label to "✓ Payment confirmed", and reveals a reset link. If it did not, the handle and fill animate back to the start — the action is *not* committed, which is the whole point of requiring a full, intentional slide. A done flag blocks further dragging once confirmed so the action cannot fire twice.

Replayable

A reset button (hidden until confirmation) calls resetSlide, animating everything back to the initial state so you can demo it repeatedly or re-arm the control after, say, a failed transaction.

Because the commit happens only in complete(), wiring it to a real action is a one-line change. Pair this with a confirm dialog for typed confirmations, an order summary before payment, or a download button for progress-based actions.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to work out the pointer math or the threshold logic by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why down() records an offset between the pointer and the handle's current position instead of just using the raw pointer coordinate, or why maxPos() is recalculated live rather than cached once. The same assistant can help optimize it, for instance checking whether the move handler could throttle its style writes on very high-frequency pointer events without hurting the 1:1 tracking feel. It is just as useful for extending the control: ask it to add a keyboard-accessible fallback with arrow-key filling and a role of slider with aria-valuenow, support a hold-at-the-end confirmation delay before locking, or add a haptic vibration call on mobile when the slide completes. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "slide to confirm" drag-to-commit control in plain HTML, CSS, and JavaScript, supporting both mouse and touch — no libraries.

Requirements:
- A pill-shaped track containing a label, a fill element, and a circular draggable handle positioned at the left edge.
- The handle's position must be driven only by a CSS transform: translateX, and the fill's growth only by transform: scaleX from a left transform-origin — do not animate raw width or left/right positioning, so the motion stays compositor-smooth and survives being exported to a utility-class framework that only animates transforms.
- On pointer-down, record the offset between the pointer's coordinate and the handle's current position so that grabbing the handle anywhere on its face does not cause it to jump to the cursor location.
- On pointer-move, compute the new position as pointerCoordinate minus that recorded offset, clamped between zero and a live-measured maximum derived from the track's current width minus the handle's width and padding — recompute this maximum on each interaction rather than caching it once, so it stays correct if the layout changes.
- Attach the move and release listeners to the window, not just the track or handle, so a drag continues tracking correctly even if the pointer leaves the track's bounds mid-gesture.
- On release, if the handle's position is within a few pixels of the maximum, lock the control into a confirmed state: snap the handle fully to the end, fill the entire track, change the label text, and block any further dragging via a boolean flag. If released short of that threshold, animate both the handle and fill back to the start with a CSS transition that is only enabled during the snap-back and confirm moments, not during active dragging.
- Provide a reset control that reverses the confirmed state and animates everything back to the starting position so the interaction can be replayed.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA dark pill track appears with a white circular handle on the left and the label "Slide to confirm payment".
  2. 2
    Drag the handlePress the handle and drag right — it follows your finger 1:1 and a green fill grows behind it.
  3. 3
    Release earlyLet go before the end — the handle and fill spring back to the start and nothing is confirmed.
  4. 4
    Slide all the wayDrag to the far right and release — the track locks green, the label becomes "✓ Payment confirmed", and a reset link appears.
  5. 5
    Reset and replayClick "↺ Reset" — everything animates back so you can try again or re-arm the control.
  6. 6
    Wire your actionPut your real API call or navigation inside complete() — it only runs on a full, intentional slide.

Real-world uses

Common Use Cases

Payment and checkout confirmation
A deliberate "slide to pay" before charging a card. Place it after an order summary so users review then commit.
Destructive action guards
Require a full slide to delete an account or wipe data; pair with a confirm dialog for an extra typed safeguard.
Dispatch and approval flows
"Slide to send", "slide to approve", or "slide to publish" in admin tools where an accidental tap would be costly. Pair reversible ones with a snackbar with undo.
Unlock and access controls
Slide-to-unlock for kiosks, lock screens, or gated content, mirroring the original mobile unlock gesture.
Emergency and high-stakes triggers
Arm or trigger an action that must not fire by accident, where the long deliberate motion is the safety mechanism.
Ride-share / delivery handoffs
"Slide to start trip" or "slide to mark delivered" — a confident, unambiguous commit gesture on mobile.

Got questions?

Frequently Asked Questions

Put your code inside complete() — it is called only when the handle is released at the end of the track. For async actions (a payment API), keep the locked state during the request and, on failure, call resetSlide() with an error message so the user can slide again; on success, leave it locked or navigate away.

Transforms (translateX, scaleX) run on the compositor for smoothness, and — crucially for this site's framework exports — Tailwind's transition utility animates transform but not raw width. Using scaleX for the fill means the snap-back animation works identically in the HTML, React, Vue, and Tailwind versions.

up() confirms when pos >= maxPos() - 4 (within 4px of the end). Lower the tolerance for a stricter "must reach the very end" feel, or raise it to forgive a slightly short slide. You can also require holding at the end briefly before committing by adding a short timer in up().

Dragging alone is not accessible, so provide a keyboard path: make the handle a focusable <button> (it is) and support Enter/Space or Arrow-Right-to-fill as an alternative that calls complete(). Add role="slider" with aria-valuenow updated as it moves, and ensure the confirmed state is announced via the label text, not colour alone.

In React, store the position in a ref (not state, to avoid re-render per frame), attach pointer listeners in a useEffect with cleanup, and set the transform via the handle ref; flip a confirmed state in complete. In Vue, use template refs and onMounted/onUnmounted. In Angular, bind in ngAfterViewInit with @ViewChild. The transform math and snap-back CSS port unchanged.