Baktiyar Ablaikhan

Pay button

Source
After bencho.dev ↗
Author
Interaction after bencho.dev
Licence
Original code — interaction after bencho.dev

Loading…

Press pay to watch the button fold into a spinner and land on a check.

Code

"use client";

import { AnimatePresence, motion } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { Surface } from "../ui";

type Phase = "idle" | "holding" | "loading" | "done";

export default function PayButton() {
  const [phase, setPhase] = useState<Phase>("idle");
  const timers = useRef<number[]>([]);
  useEffect(() => () => timers.current.forEach((t) => window.clearTimeout(t)), []);

  const later = (fn: () => void, ms: number) => timers.current.push(window.setTimeout(fn, ms));

  const pay = () => {
    if (phase === "loading") return;
    if (phase === "done") {
      setPhase("idle");
      return;
    }
    setPhase("loading");
    later(() => setPhase("done"), 1600);
    later(() => setPhase((p) => (p === "done" ? "idle" : p)), 4600);
  };

  const round = phase === "loading" || phase === "done";

  return (
    <Surface>
      <div className="flex w-[18em] flex-col items-stretch gap-[0.9em] rounded-[1.3em] bg-white p-[1.2em] shadow-[0_12px_32px_-18px_rgba(0,0,0,0.35)]">
        <div className="flex items-center justify-between text-[0.8em] text-[#8a8984]">
          <span>Order #4821</span>
          <span>2 items</span>
        </div>
        <div className="flex items-baseline justify-between">
          <span className="text-[0.9em]">Total</span>
          <span className="text-[1.6em] font-medium tabular-nums">$48.00</span>
        </div>
        <div className="flex h-[3.1em] justify-center">
          <motion.button
            type="button"
            onClick={pay}
            onPointerDown={() => phase === "idle" && setPhase("holding")}
            onPointerLeave={() => phase === "holding" && setPhase("idle")}
            animate={{
              width: round ? "3.1em" : "100%",
              backgroundColor: phase === "done" ? "#2B46B8" : "#151515",
              scale: phase === "holding" ? 0.96 : 1,
            }}
            transition={{ type: "spring", stiffness: 320, damping: 28 }}
            className="relative grid h-full place-items-center overflow-hidden rounded-full text-white"
          >
            <AnimatePresence mode="wait" initial={false}>
              {phase === "loading" ? (
                <motion.svg key="spin" viewBox="0 0 24 24" className="h-[1.4em] w-[1.4em]" initial={{ opacity: 0 }} animate={{ opacity: 1, rotate: 360 }} exit={{ opacity: 0 }} transition={{ rotate: { repeat: Infinity, duration: 0.8, ease: "linear" } }}>
                  <circle cx="12" cy="12" r="9" fill="none" stroke="rgba(255,255,255,0.25)" strokeWidth="2.6" />
                  <path d="M21 12a9 9 0 0 0-9-9" fill="none" stroke="#fff" strokeWidth="2.6" strokeLinecap="round" />
                </motion.svg>
              ) : phase === "done" ? (
                <motion.svg key="ok" viewBox="0 0 24 24" className="h-[1.4em] w-[1.4em]" initial={{ scale: 0.4, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ opacity: 0 }}>
                  <motion.path d="M5 12.5 10 17.5 19 7" fill="none" stroke="#fff" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" initial={{ pathLength: 0 }} animate={{ pathLength: 1 }} transition={{ duration: 0.35, delay: 0.1 }} />
                </motion.svg>
              ) : (
                <motion.span key="label" className="whitespace-nowrap text-[0.95em] font-medium" initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -6 }}>
                  Pay $48.00
                </motion.span>
              )}
            </AnimatePresence>
          </motion.button>
        </div>
        <div className="h-[1.2em] text-center text-[0.75em] text-[#8a8984]">
          <AnimatePresence mode="wait">
            <motion.p key={phase === "done" ? "d" : phase === "loading" ? "l" : "i"} initial={{ opacity: 0, y: 4 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -4 }}>
              {phase === "done" ? "Paid — receipt on its way" : phase === "loading" ? "Processing…" : "Visa •••• 4242"}
            </motion.p>
          </AnimatePresence>
        </div>
      </div>
    </Surface>
  );
}
Prompt

Build "Pay button", a button animation. What it does: Press pay to watch the button fold into a spinner and land on a check. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS, using 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).