Card stack

Review a deck one card at a time, with a throw and an undo.

gesturemotiontriage
Preview
Card stack preview
🌊

Lisbon

Seven hills, ocean light

Drag the top card, or use the left and right arrow keys to skip or shortlist it. Backspace undoes the last decision.

When to use

  • One-by-one triage such as shortlisting, review queues, or onboarding picks.
  • Decks where swiping left or right should be undoable.

When not to use

  • Use a list or sortable-data-table when people compare items side by side.
  • Use carousel to browse items without deciding.
  • Use swipe-actions for quick actions on rows in a list.

shadcn CLI

Also installs (1)

  • manicat-foundation
Manual install(+)
registry.jsonjson
{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "card-stack",
  "type": "registry:ui",
  "title": "Card stack",
  "description": "Review a deck one card at a time, with a throw and an undo.",
  "categories": [
    "data"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://manicat.viraza.net/r/manicat-foundation.json"
  ],
  "files": [
    {
      "path": "registry/components/card-stack/card-stack.tsx",
      "type": "registry:component",
      "target": "@components/manicat/card-stack/card-stack.tsx"
    },
    {
      "path": "registry/components/card-stack/card-stack.module.css",
      "type": "registry:component",
      "target": "@components/manicat/card-stack/card-stack.module.css"
    }
  ],
  "docs": "Docs and live preview: https://uiarc.dev/components/card-stack",
  "meta": {
    "tier": "free",
    "kind": "component",
    "docs": "https://manicat.viraza.net/components/card-stack",
    "markdown": "https://uiarc.dev/components/card-stack/markdown",
    "tags": [
      "gesture",
      "motion",
      "triage"
    ]
  }
}

Usage

Install the component, then copy this into your app. Change the content and the props to fit your screen.

example.tsxtsx
"use client";

import { CardStack } from "@/components/manicat/card-stack/card-stack";

type Destination = { name: string; tag: string; emoji: string };

const destinations: Destination[] = [
  { name: "Lisbon", tag: "Seven hills, ocean light", emoji: "🌊" },
  { name: "Kyoto", tag: "Temples and quiet lanes", emoji: "⛩️" },
  { name: "Reykjavik", tag: "Lava fields, hot springs", emoji: "🌋" },
  { name: "Auckland", tag: "Harbour and rainforest", emoji: "⛵" },
];

export function CardStackExample() {
  return (
    <CardStack
      items={destinations}
      getKey={(item) => item.name}
      getLabel={(item) => item.name}
      labels={{ left: "Skip", right: "Shortlist" }}
      outcomes={{ left: "skipped", right: "shortlisted" }}
      label="Trip ideas"
      renderCard={(item) => (
        <div className="flex aspect-[5/3] flex-col justify-between p-6">
          <span className="text-4xl">{item.emoji}</span>
          <div>
            <p className="text-lg font-semibold">{item.name}</p>
            <p className="text-sm text-(color:--text-muted)">{item.tag}</p>
          </div>
        </div>
      )}
      renderEmpty={(reset) => (
        <div className="flex flex-col items-center gap-3 py-10 text-center">
          <p className="text-sm text-(color:--text-muted)">Every idea was decided.</p>
          <button type="button" className="text-sm font-medium text-(color:--foreground)" onClick={reset}>
            Start over
          </button>
        </div>
      )}
    />
  );
}

Examples

Real screens rather than isolated props. Switch to Code to copy a whole example into your project.

Trip shortlist Swipe right to shortlist, left to skip; an empty state offers to start over.

Trip shortlist Swipe right to shortlist, left to skip; an empty state offers to start over. preview
🌊

Lisbon

Seven hills, ocean light

Drag the top card, or use the left and right arrow keys to skip or shortlist it. Backspace undoes the last decision.

Keyboard interactions

KeysAction
ArrowLeft / ArrowRightDecides the top card left or right when the stack is focused.
Backspace / Cmd+Z / Ctrl+ZUndoes the last decision.

Accessibility

  • The stage is a focusable role="group" with aria-roledescription="card stack" and a hidden hint describing drag and keys.
  • Each card is labelled like "Lisbon, 1 of 3".
  • Decisions, undo, and reset are announced in a polite status region.
  • Buttons mirror every gesture, so swiping is never required.

Motion

  • The top card follows the drag with tilt and stamps; a flick or far drag throws it off with its velocity.
  • Cards beneath rise into place, and undo flies a card back from where it left.
  • Reduced motion fades cards out and in at home without travel.

Responsive behavior

  • The stage is min(100%, 20rem) wide; below 26rem the cards get shorter, and below 22.5rem Undo becomes icon-only.
  • Cards use touch-action pan-y, so vertical swipes still scroll the page.

Performance

  • Only the top four cards render in the stage, however long the deck.
  • Drag and throw run on motion values without per-frame React updates.

Notes for AI

  • Use for one-by-one triage: shortlisting, review queues, onboarding picks. Use a list or sortable-data-table when people compare items side by side.
  • Persist decisions from onDecide and reverse them in onUndo.