MynduiMyndui
0

Anatomy of the Slide Confirm Button

How a drag-to-confirm thumb measures its own track, trails a fill behind an x motion value, and commits past a 90% line.

01/03The track measures itself
Track
rounded-full border, bg-muted
Fill
trails thumb, width = x + thumb
Thumb
draggable, x is a motion value
Label
fades over first 60% of travel
tsx
React.useLayoutEffect(() => {const el = trackRef.current;if (!el) return;const measure = () => {  const usable = el.clientWidth - dims.thumb - dims.pad * 2;  setMaxX(Math.max(0, usable));};measure();const ro = new ResizeObserver(measure);ro.observe(el);return () => ro.disconnect();}, [dims.thumb, dims.pad]);
01

The track measures itself

Dragging is the most deliberate confirmation gesture there is — you can't mash it, and you can feel exactly how far you've committed before you let go. The whole thing is one motion value, x, and a track that measures itself.

maxX — how far the thumb can travel — isn't a fixed number. It's trackWidth - thumb - pad × 2, recomputed by a ResizeObserver so the button stays correct if its container resizes.

At the md size that's a real 288px track, a 40px thumb, 4px of pad on each side — so maxX comes out to 240. Everything below uses those exact numbers.

The fill is a second absolutely-positioned pill, pinned at top/bottom/ left: pad, whose width is derived straight from x:

tsx
 
const fillWidth = useTransform(x, (v) => v + dims.thumb);

At rest (x = 0) the fill's width equals the thumb's width, so it sits exactly under the thumb with no bleed on either side. Drag the thumb 120px right and the fill is 160px wide — it always trails the thumb's trailing edge by exactly pad. The label centered over the track fades out over the first 60% of the travel, so by the time you're most of the way across, the instruction has already gotten out of the way:

tsx
 
const labelOpacity = useTransform(x, [0, Math.max(1, maxX * 0.6)], [1, 0]);
02/03The threshold
Falls short
released below 216px → spring(500,40) back to 0
Past the line
released past 216px → confirm() springs to maxX
Threshold
maxX × 0.9 = 216px
tsx
const handleDragEnd = () => {if (statusRef.current === "confirmed") return;if (maxX > 0 && x.get() >= maxX * threshold) confirm();else settleBack();};
02

The threshold

threshold defaults to 0.9 — the thumb has to clear 90% of maxX before release counts as a confirm. Everything before that line springs back; everything past it springs the rest of the way home.

Both outcomes use the same spring — stiffness: 500, damping: 40 — just aimed at different targets. A failed drag springs x back to 0; a confirmed one springs it the rest of the way to maxX, so the last few percent of the gesture finishes itself:

tsx
 
const confirm = () => {
  setStatus("confirmed");
  animate(x, maxX, { type: "spring", stiffness: 500, damping: 40 });
  onConfirm?.();
};

const settleBack = () => {
  animate(
    x,
    0,
    reduce ? { duration: 0.2 } : { type: "spring", stiffness: 500, damping: 40 },
  );
};

reduce — this component's own useReducedMotion() check — swaps that same spring for a flat 0.2s tween on the settle-back path, so a reduced-motion user still gets an instant, legible snap instead of a bounce.

Two Framer Motion drag props shape how the thumb feels under a real finger:

tsx
 
drag={confirmed || disabled ? false : "x"}
dragConstraints={{ left: 0, right: maxX }}
dragElastic={0.04}
dragMomentum={false}

dragElastic={0.04} lets the thumb creep just slightly past 0 or maxX under drag pressure — a tiny give so the track doesn't feel like a hard wall — while dragConstraints still clamps the resting range. dragMomentum={false} turns off inertial coasting after release entirely: without it, a fast flick could carry x past the threshold on momentum alone, which would make "past the line" mean something different depending on drag speed instead of drag distance. Distance is the only thing that should decide a confirm.

03/03Result
Slide to deploy
Slide to delete
03

The result

A measured track, a fill derived from one motion value, a threshold that picks the target for the same spring, and a drag config tuned so distance — not speed — is what commits. Drag it, or just press the arrow key.

Keyboard confirms instantly

The thumb is a real <button>, and Enter / Space / ArrowRight all call confirm() directly — no simulated dragging, no waiting to cross a threshold that doesn't apply to a keypress:

tsx
 
const handleKeyDown = (event) => {
  if (disabled || statusRef.current === "confirmed") return;
  if (event.key === "Enter" || event.key === " " || event.key === "ArrowRight") {
    event.preventDefault();
    confirm();
  }
};

ArrowRight earning its own case matters here — it's the key that semantically matches "slide right to confirm," so a keyboard user gets a gesture-appropriate shortcut instead of only the generic activation keys.

Motion Score

Slide Confirm ButtonSSCompositor-only
SopacityFade / cross-fade
SpathLengthSVG pathLength reveal
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →