Baktiyar Ablaikhan

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).