Baktiyar Ablaikhan

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