Card switcher
FreeAn app switcher for open pages. Cards fan out in depth by recency: the page you are on is in front and each step back is one page older. Flick a card up to close it, press the front card to open it full size.
pnpm dlx shadcn@latest add @caliper/card-switcherWhen to use #
Use it for
- Mobile-style multitasking: open documents, tabs or conversations.
- Reviewing a handful of recent items and dropping the ones you are done with.
Skip it when
- Long lists; use a list with search.
- Anything that needs all items visible at once.
Decisions #
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- Every gesture has a key and a button
- Arrow keys move, Enter opens, Delete closes, Escape returns. Previous, Next and Open are plain buttons and a count reads, for example, 4 open · 2 of 4.
- Closing is yours to confirm
- The card flicks away, then onClose tells you which page it was. You remove it from pages, and the rest close the gap.
Usage
- Empty can offer a way back
- Pass onReopen and the empty switcher shows Reopen all.
Installation #
pnpm dlx shadcn@latest add @caliper/card-switcher
Adds its 7 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.
import { useState } from "react" import { CardSwitcher, type SwitcherPage } from "@/components/ui/card-switcher" const openPages: SwitcherPage[] = [ { id: "inbox", title: "Inbox", url: "mail.halden.app", content: "Twelve unread messages. The newest is from Priya about Thursday's failover drill." }, { id: "calendar", title: "Calendar", url: "cal.halden.app", content: "Four events today. Next up: design review at 14:00." }, { id: "docs", title: "Docs", url: "docs.halden.app", content: "Runbook: database failover. Last edited by Priya N. on October 14." }, { id: "board", title: "Board", url: "board.halden.app", content: "Sprint 42: 9 of 14 cards done, 2 blocked." }, { id: "reports", title: "Reports", url: "reports.halden.app", content: "Weekly signups are up 12 percent on last week." }, ]; export function CardSwitcherDemo() { const [pages, setPages] = useState(openPages); const [current, setCurrent] = useState("docs"); return ( <div className="w-full max-w-xl"> <CardSwitcher label="Open pages" pages={pages} current={current} onCurrentChange={setCurrent} onClose={(id) => setPages((cur) => cur.filter((page) => page.id !== id))} onReopen={() => { setPages(openPages); setCurrent("inbox"); }} /> </div> ); }
API reference #
CardSwitcher
component
Accessibility #
It exposes group and region semantics, with aria-label, aria-describedby, aria-hidden and aria-live kept in step with state. Changes that matter are announced through a live region.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.