Slide to confirm
- Source
- After bencho.dev ↗
- Author
- Interaction after bencho.dev
- Licence
- Original code — interaction after bencho.dev
Loading…
Drag the knob all the way across to confirm the payment.
Code
"use client";
import { animate, motion, useMotionValue, useTransform } from "motion/react";
import { Check, ChevronsRight } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { Surface, useSize } from "../ui";
export default function SlideToConfirm() {
const track = useRef<HTMLDivElement>(null);
const x = useMotionValue(0);
const size = useSize(track);
const max = Math.max(0, size.w - size.h);
const [done, setDone] = useState(false);
const maxMv = useMotionValue(0);
useEffect(() => maxMv.set(max), [max, maxMv]);
const progress = useTransform(() => (maxMv.get() > 0 ? Math.min(1, Math.max(0, x.get() / maxMv.get())) : 0));
const labelOpacity = useTransform(progress, [0, 0.6], [1, 0]);
const fillWidth = useTransform(x, (v) => `calc(${v}px + 3.6em - 8px)`);
return (
<Surface>
<div className="flex flex-col items-center gap-[1em]">
<motion.div
ref={track}
animate={{ backgroundColor: done ? "#2B46B8" : "#151515" }}
className="relative h-[3.6em] w-[19em] overflow-hidden rounded-full p-[4px]"
>
<motion.div className="absolute inset-y-[4px] left-[4px] rounded-full bg-white/10" style={{ width: fillWidth }} />
<motion.span
className="pointer-events-none absolute inset-0 grid place-items-center text-[0.9em] font-medium"
style={{ opacity: labelOpacity }}
>
{done ? "" : <span className="bencho-shimmer-dark">Slide to send $240</span>}
</motion.span>
{done && (
<motion.span initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} className="absolute inset-0 grid place-items-center text-[0.9em] font-medium text-white">
Sent
</motion.span>
)}
<motion.button
type="button"
data-knob
drag={done ? false : "x"}
dragConstraints={{ left: 0, right: max }}
dragElastic={0.05}
dragMomentum={false}
style={{ x }}
onDragEnd={() => {
if (x.get() > max * 0.86) {
animate(x, max, { type: "spring", stiffness: 500, damping: 40 });
setDone(true);
} else animate(x, 0, { type: "spring", stiffness: 400, damping: 26 });
}}
onClick={() => {
if (!done) return;
setDone(false);
animate(x, 0, { type: "spring", stiffness: 300, damping: 28 });
}}
whileTap={{ scale: 0.94 }}
className="relative z-10 grid h-full aspect-square cursor-grab touch-none place-items-center rounded-full bg-white text-[#151515] active:cursor-grabbing"
>
{done ? <Check className="h-[1.2em] w-[1.2em] text-[#2B46B8]" strokeWidth={2.6} /> : <ChevronsRight className="h-[1.2em] w-[1.2em]" />}
</motion.button>
</motion.div>
<p className="font-mono text-[0.7em] text-[#8a8984]">{done ? "tap the check to reset" : "drag the knob all the way"}</p>
</div>
</Surface>
);
}
Prompt
Build "Slide to confirm", a button animation. What it does: Drag the knob all the way across to confirm the payment. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS, using lucide-react, motion. Technique: an HTML canvas, the Motion library, pointer tracking. It sits on a light background, in a 16:10 frame. Respect prefers-reduced-motion with a still state, pause work when the element is off screen, and keep it accessible. Original: https://bencho.dev (Interaction after bencho.dev, Original code — interaction after bencho.dev).