Card stack
FreeA pile of cards you flick through one by one.
pnpm dlx shadcn@latest add https://caliperui.site/r/card-stack.jsonCopy
Rename Save to Save draft
Two reviewers read “Save” as publishing. A clearer label cuts support questions.
Swipe, use arrow keys, or press a button.
1 of 5
When to use #
Use it for
- Reviewing a short queue, such as suggestions or onboarding tips.
- Touch-first interfaces.
Skip it when
- Users need to compare items side by side.
Installation #
terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/card-stack.json
Adds its 3 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
example.tsx
import { useState } from "react" import { CardStack } from "@/components/ui/card-stack" const suggestions = [ { id: "s1", label: "Rename Save to Save draft", body: "Two reviewers read “Save” as publishing. A clearer label cuts support questions.", tag: "Copy" }, { id: "s2", label: "Move filters above the table", body: "Filters sit under the fold on laptops, so people scroll before they can narrow results.", tag: "Layout" }, { id: "s3", label: "Raise helper text contrast", body: "Muted helper text measures 3.9:1 on the surface. Aim for 4.5:1.", tag: "Accessibility" }, { id: "s4", label: "Add undo to bulk archive", body: "Archiving 40 rows has no way back. A toast with Undo covers the mistake.", tag: "Safety" }, { id: "s5", label: "Collapse the settings sidebar by default", body: "Most visits are to a single page. Keep the rail but start it narrow.", tag: "Layout" }, ]; export function CardStackDemo() { const [, setLog] = useState<string[]>([]); return ( <CardStack className="max-w-md" label="Review suggestions" leftLabel="Skip" rightLabel="Keep" items={suggestions.map((s) => ({ id: s.id, label: s.label, content: ( <div className="grid h-full content-between gap-4 p-5"> <span className="w-fit rounded-control bg-muted px-2 py-0.5 text-xs text-muted-foreground">{s.tag}</span> <div> <h4 className="text-base font-medium">{s.label}</h4> <p className="mt-2 text-sm text-muted-foreground">{s.body}</p> </div> <p className="text-xs text-muted-foreground">Swipe, use arrow keys, or press a button.</p> </div> ), }))} onDecision={(item, dir) => setLog((l) => [`${dir === "right" ? "Kept" : "Skipped"}: ${item.label}`, ...l].slice(0, 3))} /> ); }
API reference #
CardStack
component
PropTypeDefault
label *string—
items *readonly StackItem[]—
leftLabelstring"Skip"
rightLabelstring"Keep"
onDecision((item: StackItem, direction: Direction) => void)—
heightstring"18rem"
classNamestring—
Accessibility #
It exposes group and status semantics, with aria-label and aria-hidden kept in step with state. Changes that matter are announced through a live region.
KeyBehaviour
←Moves to the previous item or value.
→Moves to the next item or value.
BackspaceRemoves the previous character or item.
Motion #
It checks prefers-reduced-motion itself and settles immediately instead of travelling. Every state is still reachable.