caliper uidocs

Book shelf

Free

A 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.

Installs components/ui/book-shelf.tsxDepends on @radix-ui/react-slot, lucide-reactMotion none in component source
pnpm dlx shadcn@latest add @caliper/book-shelf
Pick a book

12 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 #

terminal
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.

example.tsx
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
PropTypeDefaultDescription
labelstring"Library shelf"Names the shelf for screen readers.
items *readonly BookShelfItem[]—
selectedstring | null—The id of the book pulled out, or null. Controlled when set.
defaultSelectedstring | nullnull
onSelect((id: string | null) => void)—
onOpen((id: string) => void)—Adds an Open button to the cover.
statusDepthStatus"ready"
messagesDepthMessages—
onRetry(() => void)—
disabledboolean—
classNamestring—

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.

KeyBehaviour
→Moves to the next item or value.
←Moves to the previous item or value.
HomeJumps to the first item or the minimum.
EndJumps to the last item or the maximum.
EscapeCloses or cancels, then returns focus to where it came from.

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.