Swipe Card Stack

Drag the top card away and it moves to the back of the deck.

01

Plan

Map goals, owners and dates in one view.

02

Build

Turn plans into shipped features, faster.

03

Launch

Coordinate every team on release day.

04

Learn

See what worked and do more of it.

Drag the top card sideways

Installation

  1. Install the dependencies

    $ npm i motion lucide-react
  2. Copy the source into components/ui/card-stack.tsx

    components/ui/card-stack.tsx
    "use client";
    
    import { motion } from "motion/react";
    import { ArrowRight } from "lucide-react";
    import { useState } from "react";
    
    type StackCard = { id: number; title: string; text: string; color: string };
    
    const defaultCards: StackCard[] = [
      { id: 1, title: "Plan", text: "Map goals, owners and dates in one view.", color: "from-blue-500 to-indigo-600" },
      { id: 2, title: "Build", text: "Turn plans into shipped features, faster.", color: "from-violet-500 to-fuchsia-600" },
      { id: 3, title: "Launch", text: "Coordinate every team on release day.", color: "from-rose-500 to-orange-500" },
      { id: 4, title: "Learn", text: "See what worked and do more of it.", color: "from-emerald-500 to-teal-600" },
    ];
    
    export function CardStack({ cards = defaultCards }: { cards?: StackCard[] }) {
      const [stack, setStack] = useState(cards);
      const next = () => setStack((s) => [...s.slice(1), s[0]]);
    
      return (
        <div className="flex flex-col items-center gap-6">
          <div className="relative h-72 w-64">
            {stack.map((card, i) => {
              const isTop = i === 0;
              return (
                <motion.div
                  key={card.id}
                  className={`absolute inset-0 flex flex-col justify-between rounded-3xl bg-gradient-to-br p-7 text-white shadow-xl ${card.color} ${isTop ? "cursor-grab active:cursor-grabbing" : ""}`}
                  style={{ zIndex: stack.length - i }}
                  animate={{ scale: 1 - i * 0.06, y: i * -16, opacity: i > 2 ? 0 : 1 }}
                  transition={{ type: "spring", stiffness: 300, damping: 26 }}
                  drag={isTop ? "x" : false}
                  dragSnapToOrigin
                  dragElastic={0.8}
                  whileDrag={{ rotate: 6, scale: 1.03 }}
                  onDragEnd={(_, info) => {
                    if (Math.abs(info.offset.x) > 90 || Math.abs(info.velocity.x) > 500) next();
                  }}
                >
                  <span className="text-sm font-medium opacity-80">0{card.id}</span>
                  <div>
                    <h3 className="text-3xl font-semibold tracking-tight">{card.title}</h3>
                    <p className="mt-2 text-white/85">{card.text}</p>
                  </div>
                </motion.div>
              );
            })}
          </div>
          <button onClick={next} className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-50">
            Next card <ArrowRight className="size-4" />
          </button>
        </div>
      );
    }
    
    export default function Demo() {
      return (
        <div className="pt-6 text-center">
          <CardStack />
          <p className="mt-3 text-xs text-zinc-400">Drag the top card sideways</p>
        </div>
      );
    }
    
  3. Use it

    The file exports the component and a Demo showing typical usage. Import whichever you need.