Book shelf
FreeA shelf of documents as spines. Width follows the page count, so a dozen fit in a row and scan like a list. Hover or focus leans a spine out; choosing one pulls it off the shelf and opens its cover and details beside it.
pnpm dlx shadcn@latest add @caliper/book-shelf12 documents. Hover or focus a spine to lean it out; press it to pull it off the shelf.
Pull a book out, then choose Open.
When to use #
Use it for
- A library of runbooks, handbooks or reports where people browse before they open.
- Showing how much there is and who owns it, with one document inspected at a time.
Skip it when
- Search results or anything people filter by text; use a list or a table.
- More than about twenty documents; the shelf scrolls sideways and loses its overview.
Decisions #
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- One tab stop, arrows between spines
- The shelf is a list of toggle buttons. Left, Right, Home and End move, Enter or Space pulls a book out, Escape puts it back, and each spine is named with title, author and page count.
- Depth is the second level of detail
- At rest a document is one spine. Pulling it forward turns it toward you and fills the cover panel; the other spines stay put, so the row never reflows.
Styling
- Spine height only separates neighbours
- Height varies by a few steps so twelve spines read as separate objects. It never encodes a value; width is the only measure, and the cover writes the numbers out.
Installation #
pnpm dlx shadcn@latest add @caliper/book-shelf
Adds its 5 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 { BookShelf, type BookShelfItem } from "@/components/ui/book-shelf" const shelf: BookShelfItem[] = [ { id: "failover", title: "Runbook: database failover", author: "Priya N.", pages: 64, summary: "Step by step, from alert to all clear. Tested in the October drill." }, { id: "design", title: "Design system handbook", author: "Mira S.", pages: 212, summary: "Tokens, patterns and the rules behind both." }, { id: "incidents", title: "Incident reviews 2026", author: "Jo K.", pages: 148, summary: "What went wrong, what we changed." }, { id: "api", title: "API style guide", author: "Aasis S.", pages: 58, summary: "Names, errors, pagination." }, { id: "onboarding", title: "Onboarding week one", author: "Sam R.", pages: 36, summary: "The first five days, hour by hour." }, { id: "pricing", title: "Pricing notes", author: "Mira S.", pages: 22, summary: "How the plans were set and why." }, { id: "security", title: "Security policy", author: "Priya N.", pages: 96, summary: "Access, keys, retention, reporting." }, { id: "voice", title: "Brand voice", author: "Jo K.", pages: 44, summary: "Plain words and how to keep them." }, { id: "release", title: "Release checklist", author: "Sam R.", pages: 18, summary: "Before every deploy." }, { id: "data", title: "Data dictionary", author: "Aasis S.", pages: 130, summary: "Every table, every column." }, { id: "support", title: "Support playbook", author: "Priya N.", pages: 72, summary: "Macros, tone and escalation." }, { id: "a11y", title: "Accessibility audit", author: "Mira S.", pages: 88, summary: "Findings and fixes, by screen." }, ]; export function BookShelfDemo() { const [opened, setOpened] = useState<string | null>(null); return ( <div className="grid w-full max-w-3xl gap-3"> <BookShelf items={shelf} onOpen={(id) => setOpened(shelf.find((book) => book.id === id)?.title ?? null)} /> <p className="m-0 min-h-5 text-[13px] text-muted-foreground" aria-live="polite">{opened ? `Opened “${opened}”.` : "Pull a book out, then choose Open."}</p> </div> ); }
API reference #
BookShelf
component
Accessibility #
It is built on native elements and keeps aria-label, aria-pressed, aria-live and aria-hidden 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.