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.
<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>
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.
BookshopDouglas 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>
<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" }
]
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>
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)
<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));