Bookshelf Tags

We're building a skeuomorphic bookshelf — wooden planks, shadows, and book covers that lift when you hover them. Cover art comes from Open Library using ISBN numbers.

It breaks down into three tags: individual books, shelves to hold them, and a bookcase to frame the whole thing.

See the bookshelf demo for a full example.

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

The Count of Monte Cristo

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

The Count of Monte Cristo
Slaughterhouse-Five
Les Miserables

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;
}

Demo

Stack a few books on a shelf inside a bookcase and you get this:

<bookcase>
  <shelf>
    <book title="To Kill a Mockingbird" isbn="9780060935467" />
    <book title="The Catcher in the Rye" isbn="9780316769488" />
    <book title="Crime and Punishment" isbn="9780486415871" />
  </shelf>
</bookcase>

Result

To Kill a Mockingbird
The Catcher in the Rye
Crime and Punishment