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