Book
Each book is a cover image sitting on the shelf. Give it an isbn and it pulls the cover from Open Library's Covers API. The title prop becomes the alt text.
The cover gets a subtle right-edge gradient to fake a spine, and a top-edge highlight that catches imaginary light from above.
<div
class="not-prose book-spine group/book relative hover:z-10 focus-within:z-10"
>
<div
class="book-cover w-[70px] h-[105px] cursor-pointer relative overflow-hidden rounded-sm"
>
<img
src="https://covers.openlibrary.org/b/isbn/{{ isbn }}-M.jpg"
alt="{{ title }}"
loading="lazy"
class="absolute inset-0 w-full h-full object-cover transition-opacity duration-300"
referrerpolicy="no-referrer"
/>
<div class="book-edge"></div>
<div
class="absolute top-0 left-0 right-0 h-1 bg-linear-to-b from-white/20 to-transparent"
></div>
</div>
</div>
Example
Shelf
A shelf is a horizontal row that holds books. The visual trick is layering: a shadow element at the top (cast by the shelf above), the books in the middle, and a wooden plank at the bottom. The plank sits on top via z-index so it looks like the books are standing on it. If there are too many books to fit, the row scrolls horizontally.
<div class="shelf-row">
<div class="shelf-shadow"></div>
<div class="books-grid flex-1">
<yield />
</div>
<div class="shelf-plank"></div>
</div>
Example
Bookcase
The bookcase wraps everything — just a vertical stack of shelves with no gap, so the planks butt up against each other.
<div class="bookcase flex flex-col gap-0">
<yield />
</div>
The styles do most of the work. We define a set of wood-tone CSS custom properties and reuse them throughout — gradients on the plank, a dark background behind the shelves, and an inset border that gives the whole thing some depth. The shelf shadow is a radial gradient fading downward, clipped into an ellipse so it reads as light falling from above. Books get a hover transform that lifts them off the shelf, and each cover has a fake spine edge on the right side.
.bookcase {
--wood-light: hsl(15, 30%, 38%);
--wood-mid: hsl(12, 28%, 28%);
--wood-dark: hsl(10, 25%, 20%);
--wood-darker: hsl(8, 22%, 14%);
--wood-grain: hsl(12, 20%, 32%);
position: relative;
background: var(--wood-dark);
border: 6px solid var(--wood-darker);
border-radius: 6px;
padding: 12px 10px 16px;
box-shadow:
inset 0 2px 8px rgba(0, 0, 0, 0.3),
0 4px 20px rgba(0, 0, 0, 0.4);
}
.bookcase::before {
content: "";
position: absolute;
inset: 0;
border-radius: 2px;
border: 2px solid var(--wood-grain);
opacity: 0.3;
pointer-events: none;
}
.shelf-row {
position: relative;
display: flex;
align-items: flex-end;
padding: 8px 6px 0;
min-height: 130px;
margin-bottom: 6px;
}
.shelf-shadow {
position: absolute;
top: 0;
left: 4px;
right: 4px;
height: 18px;
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.3),
rgba(0, 0, 0, 0.08) 50%,
transparent
);
border-radius: 0 0 50% 50% / 0 0 100% 100%;
pointer-events: none;
z-index: 1;
}
.books-grid {
display: flex;
align-items: flex-end;
gap: 8px;
padding-bottom: 4px;
position: relative;
z-index: 2;
overflow-x: auto;
justify-content: safe center;
scrollbar-width: thin;
scrollbar-color: #888 transparent;
scrollbar-gutter: stable;
}
.shelf-plank {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 12px;
background: linear-gradient(
to bottom,
var(--wood-light) 0%,
var(--wood-mid) 35%,
var(--wood-dark) 75%,
var(--wood-darker) 100%
);
z-index: 3;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.35);
}
.shelf-plank::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(
to right,
transparent 0%,
rgba(255, 255, 255, 0.2) 15%,
rgba(255, 255, 255, 0.3) 50%,
rgba(255, 255, 255, 0.2) 85%,
transparent 100%
);
}
.book-spine {
transition: transform 0.2s ease;
}
.book-spine:hover {
transform: translateY(-6px);
}
.book-cover {
box-shadow:
1px 1px 3px rgba(0, 0, 0, 0.4),
-1px 0 2px rgba(0, 0, 0, 0.15);
}
.book-edge {
position: absolute;
top: 2px;
right: 0;
bottom: 2px;
width: 3px;
background: linear-gradient(
to right,
rgba(0, 0, 0, 0.1),
rgba(255, 255, 255, 0.08) 40%,
rgba(0, 0, 0, 0.15)
);
pointer-events: none;
}