Skip to content
motion basics
Back to the collection

STUDY / 004 · Interactions

Card stack

Layers of ideas. A new one with every tap.

Transforms

Preview

Live preview · Go ahead, interact with it

Code

"use client";
import { ArrowUpRight, Layers } from "lucide-react";
import { motion, useReducedMotion } from "motion/react";
import { useState } from "react";
import { cn } from "@/lib/utils";

const cards = [
  {
    title: "Small details.",
    subtitle: "Big difference.",
    color: "bg-[#eae6df]",
  },
  {
    title: "Less, but better.",
    subtitle: "Room to breathe.",
    color: "bg-[#e4e9df]",
  },
  {
    title: "Made to move.",
    subtitle: "A work in progress.",
    color: "bg-[#e1e7ee]",
  },
];
export default function CardStack({ className }: { className?: string }) {
  const [front, setFront] = useState(0);
  const reduced = useReducedMotion();
  return (
    <button
      type="button"
      aria-label="Cycle card stack"
      onClick={() => setFront((front + 1) % cards.length)}
      className={cn(
        "relative h-48 w-60 text-left outline-offset-12",
        className,
      )}
    >
      {cards.map((card, index) => {
        const position = (index - front + cards.length) % cards.length;
        return (
          <motion.span
            key={card.title}
            animate={{
              y: reduced ? 0 : -position * 12,
              scale: 1 - position * 0.06,
              rotate: reduced ? 0 : position * -3,
            }}
            transition={{
              type: "spring",
              duration: reduced ? 0 : 0.4,
              bounce: 0.12,
            }}
            style={{ zIndex: cards.length - position }}
            className={cn(
              "absolute inset-0 flex flex-col justify-between rounded-xl border border-black/10 p-5 shadow-[0_4px_12px_-6px_#0002]",
              "text-neutral-900",
              card.color,
            )}
          >
            <span className="flex justify-between">
              <Layers size={19} strokeWidth={1.5} />
              <ArrowUpRight size={15} />
            </span>
            <span>
              <span className="block text-xl font-medium tracking-tight">
                {card.title}
              </span>
              <span className="mt-1 block text-xs text-neutral-500">
                {card.subtitle}
              </span>
            </span>
            <span className="font-mono text-[9px] text-neutral-500">
              STUDY 00{index + 1}
            </span>
          </motion.span>
        );
      })}
    </button>
  );
}

React · TypeScript · Tailwind CSS

Copy the component and the cn helper into your project.

bun add motion lucide-react clsx tailwind-merge