Scroll Book Shelf Slide — Free HTML CSS JS Snippet
Scroll Book Shelf Slide · Scroll · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Scroll Book Shelf Slide — GSAP Timeline Segments, Shelf Illustration & Content Panel Reveal

This snippet turns a row of styled div "book spines" into a scroll-driven storytelling shelf: as the user scrolls through a tall pinned-feeling section, each book in turn slides outward and tilts off the shelf while a hinged "pages" panel unfolds beside it to reveal a title and description, then both settle back before the next book takes its turn.
One shared scrub timeline, sliced into segments
A single GSAP timeline is attached to .shelf-wrap with scrollTrigger: { start: 'top top', end: 'bottom bottom', scrub: true }. Rather than creating one ScrollTrigger per book, the total scroll distance is divided into equal fractions — segStart = i / books.length and segEnd = (i + 0.85) / books.length — and each book's slide/tilt and its pages panel's open/close tweens are placed on the shared timeline at those absolute positions using GSAP's position parameter. This keeps every book perfectly in sync with scroll position without juggling multiple triggers.
The slide-and-tilt
Each .book is a narrow div styled as a spine with transform-origin: bottom center so it pivots naturally like a book being pulled forward. gsap.fromTo animates x, y and rotate from resting values to x: 34, y: -10, rotate: -7 — enough to read as "sliding outward and tipping" without leaving the shelf entirely.
The pages panel
The .pages div is absolutely positioned to the right of the spine with transform-origin: left center. It starts at opacity: 0, scaleX: 0.2, translated slightly left, then scales up to scaleX: 1 with full opacity — mimicking a book cover swinging open — timed to appear once the spine has slid far enough to have "room," and closing again as the segment ends so the effect fully reverses on scroll-up.
Fully reversible, no pinning required
Because everything lives on one scrubbed timeline keyed to scroll progress (not pin: true), scrolling back up smoothly reverses every book in sequence — GSAP scrub timelines are inherently bidirectional, so no manual reset logic is needed.
Pair this with Scroll Card Deck Shuffle for another sequential reveal pattern, or Scroll Polaroid Stack Flip for a similarly staggered "stack" story.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Paste this snippet's HTML, CSS and JS into an AI assistant and ask it to walk through why the segment boundaries are computed as i / books.length and (i + 0.85) / books.length rather than fixed pixel or percentage values — the answer touches on why fraction-based segmentation keeps the effect correct regardless of how many books you add or remove. It's also a good candidate to extend: ask the assistant to make the pages panel display a real cover image with a CSS 3D flip instead of a flat scale-open, to add a subtle shelf-dust parallax layer behind the books, or to convert the shelf into a horizontal-scroll gallery using the same segment-timeline technique paired with scroll-horizontal-pin.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a scroll-driven "bookshelf" animation in HTML, CSS and JavaScript using GSAP and ScrollTrigger — no React, no canvas, no WebGL.
Requirements:
- Render a horizontal row of narrow div "book spine" elements sitting on a wooden shelf illustration built from CSS gradients, each spine a different flat color and each with a vertical text label (use CSS writing-mode).
- Give each book a hidden absolutely-positioned "pages" panel anchored to its right edge, starting scaled down and transparent with its transform-origin on the left edge so it can open like a hinged cover.
- Wrap the whole shelf in a tall section (several viewport heights) and attach exactly one GSAP timeline to it via ScrollTrigger with start 'top top', end 'bottom bottom', and scrub: true.
- Divide the timeline into one segment per book using fractions of the book count (segment i spans from i/total to (i+1)/total of the timeline), and within each book's segment: slide and rotate the spine outward from the shelf using its own transform-origin at the bottom, then fade/scale open its pages panel, then close the panel and return the spine to rest before the next book's segment begins — placing every tween at its correct start time with GSAP's timeline position parameter.
- The entire effect must play forward and reverse cleanly as the user scrolls down and back up, since scrub ties it directly to scroll position with no manual state to reset.
- Use a warm library color palette: dark wood browns for the shelf and background, and a distinct accent hue per book spine.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Source Code
<div class="hint">Scroll ↓ to pull books off the shelf</div>
<div class="shelf-wrap">
<div class="shelf-stage">
<div class="shelf-board"></div>
<div class="books">
<div class="book" data-title="Deep Work" style="--hue:210;--i:0">
<span class="spine-label">Deep Work</span>
<div class="pages">
<h3>Deep Work</h3>
<p>Rules for focused success in a distracted world. Cultivate the ability to concentrate without distraction on cognitively demanding tasks.</p>
</div>
</div>
<div class="book" data-title="Atomic Habits" style="--hue:0;--i:1">
<span class="spine-label">Atomic Habits</span>
<div class="pages">
<h3>Atomic Habits</h3>
<p>An easy and proven way to build good habits and break bad ones. Tiny changes, remarkable results.</p>
</div>
</div>
<div class="book" data-title="Sapiens" style="--hue:35;--i:2">
<span class="spine-label">Sapiens</span>
<div class="pages">
<h3>Sapiens</h3>
<p>A brief history of humankind, from foraging bands to the cognitive, agricultural and scientific revolutions.</p>
</div>
</div>
<div class="book" data-title="The Design of Everyday Things" style="--hue:150;--i:3">
<span class="spine-label">The Design of Everyday Things</span>
<div class="pages">
<h3>The Design of Everyday Things</h3>
<p>Why some products satisfy customers while others frustrate them — the psychology of good design.</p>
</div>
</div>
<div class="book" data-title="Thinking, Fast and Slow" style="--hue:275;--i:4">
<span class="spine-label">Thinking, Fast and Slow</span>
<div class="pages">
<h3>Thinking, Fast and Slow</h3>
<p>Two systems drive the way we think — fast, intuitive and emotional, versus slower, deliberate and logical.</p>
</div>
</div>
</div>
</div>
</div>* { box-sizing: border-box; }
body { margin: 0; font-family: 'Georgia', 'Times New Roman', serif; background: linear-gradient(#2b1c11, #1a1108); color: #f4e9d8; }
.hint { text-align: center; padding: 28px 16px; font-size: 14px; letter-spacing: 0.06em; color: #c9a876; text-transform: uppercase; font-family: system-ui, sans-serif; }
.shelf-wrap { height: 600vh; position: relative; }
.shelf-stage { position: sticky; top: 0; height: 100vh; display: flex; align-items: center; justify-content: center; overflow: hidden; background: radial-gradient(ellipse at 50% 30%, #3a2513, #1a1108 75%); }
.shelf-board { position: absolute; bottom: 18%; left: 8%; right: 8%; height: 22px; background: linear-gradient(180deg, #6b4226, #3f260f); border-radius: 3px; box-shadow: 0 18px 30px rgba(0,0,0,0.6), inset 0 2px 0 rgba(255,255,255,0.08); z-index: 1; }
.books { position: relative; display: flex; align-items: flex-end; gap: 6px; bottom: 18%; perspective: 1000px; z-index: 2; }
.book { position: relative; width: 46px; height: 280px; background: linear-gradient(90deg, hsl(var(--hue) 45% 32%), hsl(var(--hue) 45% 42%)); border-radius: 3px 5px 5px 3px; box-shadow: inset -3px 0 6px rgba(0,0,0,0.35), inset 3px 0 4px rgba(255,255,255,0.15), 0 8px 16px rgba(0,0,0,0.45); transform-origin: bottom center; display: flex; align-items: center; justify-content: center; }
.spine-label { writing-mode: vertical-rl; transform: rotate(180deg); font-size: 13px; font-weight: 700; letter-spacing: 0.04em; color: #f4e9d8; text-shadow: 0 1px 2px rgba(0,0,0,0.4); white-space: nowrap; }
.pages { position: absolute; left: 100%; bottom: 0; width: 260px; height: 100%; background: #f4ecd8; color: #2b1c11; border-radius: 0 10px 10px 0; box-shadow: 6px 10px 24px rgba(0,0,0,0.5); padding: 26px 22px; opacity: 0; transform: translateX(-30px) scaleX(0.2); transform-origin: left center; pointer-events: none; }
.pages h3 { margin: 0 0 12px; font-size: 19px; color: hsl(var(--hue) 45% 28%); }
.pages p { margin: 0; font-size: 14px; line-height: 1.7; }
@media (max-width: 640px) {
.book { width: 34px; height: 220px; }
.pages { width: 190px; padding: 18px 16px; }
.pages p { font-size: 12.5px; }
}gsap.registerPlugin(ScrollTrigger);
const books = gsap.utils.toArray('.book');
const stage = document.querySelector('.shelf-stage');
books.forEach((book, i) => {
const pages = book.querySelector('.pages');
const tl = gsap.timeline({
scrollTrigger: {
trigger: '.shelf-wrap',
start: 'top top',
end: 'bottom bottom',
scrub: true,
},
});
const segStart = i / books.length;
const segEnd = (i + 0.85) / books.length;
tl.fromTo(book,
{ x: 0, y: 0, rotate: 0 },
{ x: 34, y: -10, rotate: -7, ease: 'none' },
segStart
)
.fromTo(pages,
{ opacity: 0, x: -30, scaleX: 0.2 },
{ opacity: 1, x: 0, scaleX: 1, ease: 'none' },
segStart + (segEnd - segStart) * 0.35
)
.to(pages, { opacity: 0, x: -30, scaleX: 0.2, ease: 'none' }, segEnd);
if (i > 0) {
tl.to(book, { x: 0, y: 0, rotate: 0, ease: 'none' }, segEnd);
}
});
ScrollTrigger.refresh();Step by step
How to Use
- 1Scroll through the previewScroll down slowly — each book spine slides forward and tilts while its content panel unfolds beside it, then both reset as the next book begins.
- 2Add or remove booksDuplicate a .book element inside .books with a unique --hue custom property for its spine color, a spine-label span, and a .pages panel with your own title/description.
- 3Tune the segment timingIn the JS panel, adjust segStart and segEnd (currently i/books.length and (i+0.85)/books.length) to control how much scroll distance each book gets and how much they overlap.
- 4Adjust the slide distance and tiltChange the x, y and rotate values inside the fromTo call to make the slide-off more or less dramatic.
- 5Restyle the shelfThe .shelf-board and .shelf-stage background gradients set the warm wood tone — swap the hsl() values or background gradient for a different library aesthetic.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
A single timeline scrubbed against one long .shelf-wrap section is more performant and keeps every book perfectly synchronized to scroll position. Each book's animation is placed on that shared timeline at a calculated fraction of the total duration using GSAP's position parameter.
Remove the final .to(pages, { opacity: 0 ... }) tween for that book's segment, or extend its segEnd close to 1 so it only closes near the very end of the section.
Yes — add an <img> inside .pages alongside or instead of the <p> text; the panel container already scales and fades in as a unit.
Because scrub: true ties the timeline's playhead directly to scroll position rather than firing a one-shot animation, GSAP automatically plays every tween in reverse as the user scrolls upward.
Any number — the segment math (i / books.length) automatically redistributes the available scroll distance evenly across however many .book elements exist.





