<book-shelf>

A zero-dependency web component: your reading list as a straight 3D stack of spines (hover lifts a book toward you, ↑/↓ to browse), with a plain list view one click away. Click a spine and the book gets a page of its own — standing up, cover and description and links — addressed by URL fragment, so it can be shared and closed with Back. No build step, no framework, one file. Shelves of any size: the stack's geometry is computed from the data up front, so long lists render lazily behind an exact-height spacer — honest scrollbar, reachable footer, no observers — and a 600-book shelf scrolls at the same frame cost as a 20-book one.

1. Inline markup

<script type="module" src="book-shelf.js"></script>

<book-shelf>
  <shelf-book href="https://amazon.com/dp/0691147728"
              author="Robert J. Gordon"
              subtitle="The U.S. Standard of Living since the Civil War">The Rise and Fall of American Growth</shelf-book>
  <shelf-book author="Shaun Tan" note="Wordless. One sitting.">
  The Arrival
  <p>A man leaves his family for a city where he cannot read the signs…</p>
  <a href="https://bookshop.org/…">Bookshop</a>
</shelf-book>
</book-shelf>
The Rise and Fall of American Growth Poor Charlie's Almanack The Dream Machine Pacific Whole Earth Discipline Where Is My Flying Car? The Arrival

A man leaves his family and his country for a city where he cannot read the signs, the animals are strange and the food is unfamiliar, and slowly makes a life there.

Shaun Tan drew the whole thing in sepia pencil, in the register of old photographs, so that an entirely invented world reads as memory. The absence of words is the point: it is what arriving feels like.

Bookshop
Labyrinths An Elegant Puzzle Gödel, Escher, Bach

Douglas Hofstadter's book about how meaning and selfhood can arise from formal systems that have neither. Gödel's incompleteness theorem, Escher's impossible staircases and Bach's fugues are the recurring examples of strange loops — structures that reach up through their own levels and refer back to themselves.

The spines above are already colour- and texture-matched to their real covers — every href is an Amazon link, and the ISBN-10 in that URL doubles as a free cover lookup against Open Library's cover API (no fetch code on our end, it's a plain image URL). The spine itself isn't that photo stretched across a bar — cramming a portrait cover into a wide short strip crops into an unreadable mess. Instead it picks a colour and a material from a finite set of textures (smooth, paper, cloth, linen, kraft, leather — pure CSS/SVG, no image assets) by reading the cover's actual pixels through an offscreen canvas: a dominant-colour vote biased toward saturated pixels, plus brightness/variance/saturation stats for the material. No cover, or the read fails → falls back to a deterministic pick from the title. Give an explicit isbn when there's no Amazon link to piggyback on, or pin either choice directly. The shelf is lit, not patterned: one lamp is defined in the scene, and the grain, the stamped title, its foil and the sheen are all solved from it with vector math — feDiffuseLighting/feSpecularLighting compute real surface normals from a height field, and each book's lighting is recomputed from its own orientation as it pitches past the eye line:

<shelf-book isbn="0143127799" author="Yuval Noah Harari">Sapiens</shelf-book>
<shelf-book color="#2b2b2e" texture="leather" author="…">…</shelf-book>

2. From JSON

<book-shelf src="/books.json" view="list"></book-shelf>

// books.json — an array, or { "books": [...] }
[
  { "title": "Labyrinths", "author": "Jorge Luis Borges",
    "href": "https://amazon.com/dp/0394604490", "color": "#4a1942" }
]

3. Progressive enhancement of a plain list

Wrap an ordinary <ul>. With JavaScript off you still get a list of links.

<book-shelf>
  <ul>
    <li data-author="Thomas S. Kuhn">
      <a href="https://amazon.com/dp/0226458121">The Structure of Scientific Revolutions</a>
    </li>
  </ul>
</book-shelf>

Fields

title      text content of <shelf-book> (or "title" in JSON)
subtitle   shown after a colon in list view and in the hover card
author     right side of the spine
href       where the spine links (opens in a new tab)
color      pins the spine colour, skipping colour inference
accent     pins the stamping ink; otherwise inferred from the cover
texture    pins the spine material: smooth | paper | cloth | linen | kraft | leather
isbn       real cover from Open Library for the top face; infers spine colour/texture
cover      image URL for the top face, overrides isbn
pages      page count → spine thickness (otherwise pseudo-random)
note       free-text annotation for the hover card / list
year       shown next to the author in list view
font       "serif" or "mono" spine lettering (--bs-font-serif / --bs-font-mono)
case       "upper" → author and title stamped in caps
mark       short text on the right of the spine (imprint, year, glyph)
description  the book's page: paragraphs separated by blank lines, or <p>s in markup
links      more links for the page, [{ label, href }] or <a>s in markup (href is the first)

Element attributes & theming

<book-shelf view="shelf|list" no-toggle>

book-shelf {
  --bs-font: inherit;          /* UI text */
  --bs-font-spine: inherit;    /* spine & cover lettering */
  --bs-font-serif / --bs-font-mono;  /* per-book font="serif|mono" faces */
  --bs-text / --bs-muted / --bs-rule
  --bs-spine-width: min(100%, 520px);
  --bs-gap: 56px;              /* air between books */
  --bs-depth: 170px;           /* how deep the slab is */
  --bs-perspective: 2600px;
}

/* deeper styling: bar, count, toggle, shelf, book, spine, tip, floor, list, empty */
book-shelf::part(tip) { font-style: italic; }

/* JS API */
const shelf = document.querySelector('book-shelf');
shelf.books = [{ title: '…', author: '…' }];   // set programmatically
shelf.view = 'list';
shelf.addEventListener('book-select', e => console.log(e.detail)); // cancelable: preventDefault() keeps the page shut
shelf.addEventListener('book-open', e => console.log('page', e.detail.slug));