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
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>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/card-stack.tsxcomponents/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> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.