# Reading Shelf

> Horizontal book-cover shelf for currently reading.

A shadcn/ui block from the [@atroui](https://www.atroui.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/atroui/reading-shelf).

- Page, with a live preview: https://shadcn.exchange/r/atroui/reading-shelf
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/atroui/reading-shelf-light-2a06d89d0aff57ef.png
- Registry item: https://www.atroui.com/r/reading-shelf.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://www.atroui.com/r/reading-shelf.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Files

### `registry/default/blocks/reading-shelf.tsx`

```tsx
const CONTENT = {
  stamp: "Reading now",
}

/** Default demo covers — typographic fallbacks (no remote images required). */
const BOOKS = [
  {
    title: "Designing Data-Intensive Applications",
    author: "Martin Kleppmann",
    cover: "",
    tone: "#1e3a5f",
  },
  {
    title: "Refactoring",
    author: "Martin Fowler",
    cover: "",
    tone: "#3d2c29",
  },
  {
    title: "The Pragmatic Programmer",
    author: "Hunt & Thomas",
    cover: "",
    tone: "#1a3d2e",
  },
]

const COVER_W = 88
const COVER_H = 128

export function ReadingShelf({
  stamp = CONTENT.stamp,
  books = BOOKS,
  className,
}: {
  stamp?: string
  books?: Array<{
    title: string
    author: string
    cover?: string
    tone?: string
  }>
  className?: string
} = {}) {
  if (books.length === 0) return null

  return (
    <section className={className ?? "mx-auto max-w-[640px]"}>
      <div className="mb-3 flex items-baseline justify-between">
        <h2 className="font-mono text-[10.5px] tracking-[0.12em] text-muted-foreground uppercase">
          {stamp}
        </h2>
        <span className="font-mono text-[11px] text-muted-foreground">
          {books.length} book{books.length === 1 ? "" : "s"}
        </span>
      </div>

      <div
        className="no-scrollbar -mx-5 flex gap-4 overflow-x-auto px-5 pb-2 sm:mx-0 sm:overflow-visible sm:px-0"
        style={{ scrollSnapType: "x mandatory" }}
      >
        {books.map((book, i) => (
          <div
            key={book.title}
            className="group relative shrink-0"
            style={{
              scrollSnapAlign: "start",
              animationDelay: `${i * 40}ms`,
            }}
            title={`${book.title} — ${book.author}`}
          >
            <div
              className="relative overflow-hidden rounded-[3px] transition-transform duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:-translate-y-[2px]"
              style={{
                width: COVER_W,
                height: COVER_H,
                boxShadow:
                  "0 1px 2px 0 rgba(0,0,0,0.08), 0 0 0 1px color-mix(in oklab, black 6%, transparent) inset",
              }}
            >
              {book.cover ? (
                <img
                  src={book.cover}
                  alt={`${book.title} by ${book.author}`}
                  width={COVER_W * 2}
                  height={COVER_H * 2}
                  className="h-full w-full object-cover"
                />
              ) : (
                <div
                  className="flex h-full w-full flex-col justify-between p-2.5"
                  style={{ background: book.tone ?? "#1a1a1a" }}
                  aria-hidden
                >
                  <span className="font-mono text-[9px] tracking-wide text-white/70 uppercase">
                    {book.author.split(" ").slice(-1)[0]}
                  </span>
                  <span className="text-[11px] leading-snug font-medium text-white">
                    {shortTitle(book.title, 28)}
                  </span>
                </div>
              )}
            </div>
            <div
              className="mt-2 truncate text-[11px] leading-tight text-muted-foreground"
              style={{ width: COVER_W }}
            >
              {shortTitle(book.title)}
            </div>
          </div>
        ))}
      </div>
    </section>
  )
}

function shortTitle(t: string, max = 22) {
  return t.length > max ? `${t.slice(0, max - 2)}…` : t
}
```
