caliper uidocs

Card stack

Free

A pile of cards you flick through one by one.

Installs components/ui/card-stack.tsxDepends on @radix-ui/react-slot, lucide-reactMotion honours reduced motion
pnpm dlx shadcn@latest add https://caliperui.site/r/card-stack.json
Copy

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.