Card swap
- Source
- After bencho.dev ↗
- Author
- Interaction after bencho.dev
- Licence
- Original code — interaction after bencho.dev
Loading…
Drag or tap the top card to send it to the back of the stack.
Code
"use client";
import { motion, type PanInfo } from "motion/react";
import { useRef, useState } from "react";
import { Surface } from "../ui";
const CARDS = [
{
name: "Mountain",
bg: "radial-gradient(60% 50% at 30% 70%, #ff6a3d 0, transparent 70%), radial-gradient(50% 40% at 70% 40%, #ff9ad5 0, transparent 70%), radial-gradient(40% 40% at 60% 85%, #2fbf71 0, transparent 70%), linear-gradient(160deg, #3d6fe0, #79a6ff)",
},
{
name: "Dune",
bg: "radial-gradient(70% 40% at 50% 80%, #e9b872 0, transparent 70%), radial-gradient(60% 50% at 20% 30%, #f6dfb5 0, transparent 70%), linear-gradient(170deg, #f2a15f, #8d4a2b)",
},
{
name: "Tide",
bg: "radial-gradient(60% 40% at 40% 60%, #9ef0e6 0, transparent 70%), radial-gradient(50% 50% at 80% 20%, #ffffff 0, transparent 60%), linear-gradient(200deg, #1b6b8f, #0d2c4a)",
},
{
name: "Bloom",
bg: "radial-gradient(45% 45% at 50% 50%, #ffd23f 0, transparent 70%), radial-gradient(70% 60% at 20% 80%, #ee4266 0, transparent 70%), linear-gradient(140deg, #540d6e, #ee4266)",
},
];
export default function CardSwap() {
const [order, setOrder] = useState([0, 1, 2, 3]);
const dragged = useRef(false);
const next = () => setOrder((o) => [...o.slice(1), o[0]]);
const tap = () => {
if (!dragged.current) next();
};
const onDragEnd = (_: unknown, info: PanInfo) => {
window.setTimeout(() => (dragged.current = false), 0);
const far = Math.hypot(info.offset.x, info.offset.y) > 70;
const fast = Math.hypot(info.velocity.x, info.velocity.y) > 500;
if (far || fast) next();
};
return (
<Surface>
<div className="relative h-[14em] w-[11em] translate-x-[-1.5em] translate-y-[2.2em]">
{CARDS.map((card, id) => {
const i = order.indexOf(id);
const top = i === 0;
return (
<motion.div
key={card.name}
initial={false}
animate={{ x: i * 22, y: i * -20, scale: 1 - i * 0.06 }}
transition={{ type: "spring", stiffness: 260, damping: 26 }}
style={{ zIndex: CARDS.length - i }}
className="absolute inset-0"
>
<motion.div
drag={top}
dragSnapToOrigin
dragElastic={0.6}
onDragEnd={top ? onDragEnd : undefined}
onDragStart={() => (dragged.current = true)}
onTap={top ? tap : undefined}
whileHover={top ? { rotate: -2, scale: 1.02 } : undefined}
whileDrag={{ scale: 1.04, rotate: 4, cursor: "grabbing" }}
style={{ background: card.bg }}
className={`absolute inset-0 overflow-hidden rounded-[1.1em] border-[0.25em] border-white shadow-[0_10px_30px_-12px_rgba(0,0,0,0.35)] ${top ? "cursor-grab touch-none" : ""}`}
>
<div className="absolute inset-0 bg-[radial-gradient(rgba(255,255,255,0.18)_1px,transparent_1px)] [background-size:6px_6px]" />
<motion.span
animate={{ opacity: i < 2 ? 1 : 0 }}
className="absolute bottom-[0.8em] left-[0.8em] rounded-full bg-white px-[0.8em] py-[0.3em] text-[0.8em] font-medium text-[#151515] shadow-sm"
>
{card.name}
</motion.span>
</motion.div>
</motion.div>
);
})}
</div>
<p className="pointer-events-none absolute bottom-[5%] font-mono text-[0.7em] text-[#8a8984]">drag or tap the top card</p>
</Surface>
);
}
Prompt
Build "Card swap", a card animation. What it does: Drag or tap the top card to send it to the back of the stack. 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).