Swipe Card Stack
A draggable deck of cards you flick away to cycle through, Tinder-style.
- Category
- Patterns
- Added
- 2026-08-21
- Deps
- 1
- Updated
- 2026-09-01
Props
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
const CARDS = [
{ id: 1, label: "Design" },
{ id: 2, label: "Build" },
{ id: 3, label: "Ship" },
];
export function SwipeCardStack({
threshold = 120,
stackDepth = 3,
}: {
threshold?: number;
/** How many cards are rendered at once. The rest wait behind them. */
stackDepth?: number;
}) {
const [cards, setCards] = useState(CARDS);
function handleDragEnd(id: number, offsetX: number) {
if (Math.abs(offsetX) > threshold) {
setCards((prev) => {
const rest = prev.filter((c) => c.id !== id);
const swiped = prev.find((c) => c.id === id)!;
return [...rest, swiped];
});
}
}
// Only the top few cards are mounted: a long deck otherwise renders every
// card behind an opaque stack nobody can see.
const visible = cards.slice(0, Math.max(1, stackDepth));
return (
<div className="relative flex h-48 w-56 items-center justify-center">
{visible
.slice()
.reverse()
.map((card, i) => {
const depth = visible.length - 1 - i;
const isTop = depth === 0;
return (
<motion.div
key={card.id}
drag={isTop ? "x" : false}
dragConstraints={{ left: 0, right: 0 }}
dragElastic={0.6}
onDragEnd={(_, info) => handleDragEnd(card.id, info.offset.x)}
animate={{ scale: 1 - depth * 0.05, y: depth * 10 }}
whileDrag={{ rotate: 8, cursor: "grabbing" }}
transition={{ type: "spring", stiffness: 300, damping: 24 }}
className="absolute h-40 w-48 cursor-grab rounded-2xl border border-black/10 bg-white shadow-xl"
style={{ zIndex: 10 - depth }}
>
<div className="flex h-full items-center justify-center">
<span className="text-xl font-bold">{card.label}</span>
</div>
</motion.div>
);
})}
</div>
);
}
About this pattern
Building a Tinder-style card deck usually means adding a dedicated gesture library; Framer Motion's own horizontal drag plus a locked drag-constraint box and a drag-end threshold check covers the whole interaction without one. Only the top card in the stack is draggable at all — the cards underneath are visually offset by scale and vertical translate based on their depth, but stay non-interactive, which avoids the stack fighting itself over z-index and pointer targets. Reach for this over a dedicated swipe library when the interaction is simple enough to hand-roll and the extra dependency isn't worth it. The detail worth knowing before treating this as a true swipe-to-discard deck: a card that clears the threshold isn't removed, it's moved to the back of the array, so the same three cards cycle indefinitely rather than the deck emptying out. If a true discard model is what's needed, that state update is the part to change.
Curator’s note
Only the top card gets drag={true} — a common mistake is making the whole stack draggable, which fights z-index and feels broken.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Drag Inertia Carousel
patternsA rail you can throw — it projects the momentum and settles on the nearest slide.
Infinite Drag Canvas
patternsAn endlessly pannable board that recycles a fixed set of tiles via modulo wrapping.
Peeking Toast Stack
patternsToasts collapsed into a depth-scaled deck that fans out on hover.