Skip to content

Swipe Card Stack

A draggable deck of cards you flick away to cycle through, Tinder-style.

Freeplayfulglass
Category
Patterns
Added
2026-08-21
Deps
1
Updated
2026-09-01
Preview
Ship
Build
Design

Props

120
3
Requires framer-motion
"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>
  );
}
Notes

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.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

Featured
New

A rail you can throw — it projects the momentum and settles on the nearest slide.

dragcarouselinertia
Featured
NewPro

An endlessly pannable board that recycles a fixed set of tiles via modulo wrapping.

draginfinitecanvas
Featured
NewPro

Toasts collapsed into a depth-scaled deck that fans out on hover.

toaststacknotification