webdevpuneet.com

Scroll Chapter Sidebar Story — Free Sticky-Nav Scrollytelling Layout (No Library)

Scroll Chapter Sidebar Story · Scroll · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<div class="css-layout">
  <aside class="css-sidebar">
    <div class="css-sidebar-inner">
      <div class="css-brand">The Long Road</div>
      <nav class="css-nav" id="cssNav">
        <a href="#chapter-1" data-chapter="chapter-1"><span class="css-num">01</span>The Departure</a>
        <a href="#chapter-2" data-chapter="chapter-2"><span class="css-num">02</span>Crossing the Delta</a>
        <a href="#chapter-3" data-chapter="chapter-3"><span class="css-num">03</span>The Long Silence</a>
        <a href="#chapter-4" data-chapter="chapter-4"><span class="css-num">04</span>Into the Highlands</a>
        <a href="#chapter-5" data-chapter="chapter-5"><span class="css-num">05</span>Arrival</a>
      </nav>
      <div class="css-progress-track"><div class="css-progress-fill" id="cssProgressFill"></div></div>
    </div>
  </aside>
  <main class="css-main">
    <section class="css-chapter" id="chapter-1">
      <span class="css-tag">Chapter 01</span>
      <h2>The Departure</h2>
      <p>The crew leaves before dawn, while the harbor is still asleep and the tide is exactly right. Nobody says much — everyone already knows what six months away means, and none of it needs saying out loud again.</p>
      <p>By the time the sun clears the breakwater, the coastline is already a thin grey line, and then it isn't there at all.</p>
    </section>
    <section class="css-chapter" id="chapter-2">
      <span class="css-tag">Chapter 02</span>
      <h2>Crossing the Delta</h2>
      <p>The river delta is wider than any chart suggested, a maze of channels that shift with every season. Twice the crew runs aground on sandbars that didn't exist the year before.</p>
      <p>A local guide, hired on a hunch at the last port, turns out to know every one of them by name.</p>
    </section>
    <section class="css-chapter" id="chapter-3">
      <span class="css-tag">Chapter 03</span>
      <h2>The Long Silence</h2>
      <p>Three weeks pass with no landmark, no signal, nothing but open water and the same routine repeated until it stops feeling like routine at all.</p>
      <p>It's during this stretch that the real work of the expedition happens — not the dramatic parts, just the daily discipline of staying the course.</p>
    </section>
    <section class="css-chapter" id="chapter-4">
      <span class="css-tag">Chapter 04</span>
      <h2>Into the Highlands</h2>
      <p>Landfall comes as a wall of green rising out of the haze. The highlands prove far steeper than the maps implied, and the last leg becomes a trek on foot.</p>
      <p>What the crew finds at the top of the ridge changes the entire purpose of the expedition.</p>
    </section>
    <section class="css-chapter" id="chapter-5">
      <span class="css-tag">Chapter 05</span>
      <h2>Arrival</h2>
      <p>The return home is quieter than the departure. There's no crowd waiting — just a harbor, a tide, and a crew that isn't quite the same one that left.</p>
      <p>The full account, with maps and photographs, took another two years to write.</p>
    </section>
  </main>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Zero dependencies
Built entirely on native IntersectionObserver, no animation library.
Sticky, not fixed, sidebar
position: sticky keeps the nav pinned only while the article is in view.
Middle-band active detection
The chapter centered in the viewport is highlighted, regardless of chapter length.
Two independent progress signals
Chapter highlight and overall progress rail track different things.
Clean smooth-scroll navigation
Sidebar clicks scroll without leaving a changing hash in the URL.
Scales to any chapter count
No hardcoded chapter total anywhere in the observer or progress logic.
Responsive single-column fallback
The sidebar hides entirely below 820px rather than cramping the layout.
Serif long-form typography
Readable line length and generous line-height tuned for extended reading.

About this UI Snippet

Scroll Chapter Sidebar Story — A Sticky Chapter Nav That Tracks Reading Position

Screenshot of the Scroll Chapter Sidebar Story snippet rendered live

Long-form narrative journalism — expedition write-ups, deep-dive features, book-style microsites — needs a different scaffold than a short scrollytelling stunt: a persistent way to see where you are in a much longer piece. This snippet builds that scaffold with a sticky sidebar chapter list, a native IntersectionObserver to track reading position, and a thin progress rail — no animation library required.

The sidebar is sticky, not fixed

.css-sidebar-inner uses position: sticky; top: 0; height: 100vh inside a full-height sidebar column, so it stays pinned in the viewport for as long as the reader is inside .css-main's grid row, then naturally scrolls away with the rest of the page once the article ends — simpler than a position: fixed sidebar, which would need manual show/hide logic to avoid floating over the intro and outro of the page.

A middle-band observer decides the active chapter, not scroll math

Rather than computing which chapter's scroll offset the current window.scrollY falls within — brittle math that breaks the moment chapter heights vary — a single IntersectionObserver with rootMargin: '-45% 0px -45% 0px' watches every .css-chapter section directly. Only the chapter genuinely centered in the vertical middle of the viewport is ever considered "current," so the sidebar highlight always matches where a reader's eyes actually are, regardless of how long any individual chapter's text runs.

Two separate progress signals, on purpose

The active-chapter highlight and the thin progress rail track two different things deliberately: the highlight answers "which chapter am I reading," computed from intersection state, while the rail answers "how far through the entire piece am I," computed from a plain window.scrollY ratio against total scrollable height. A reader deep in a long chapter four sees both — chapter four highlighted, and the rail two-thirds full — which a single combined indicator couldn't express as clearly.

Click-to-scroll without a hash jump

Sidebar links intercept their click with preventDefault() and call scrollIntoView({ behavior: 'smooth' }) directly on the target chapter, rather than letting the browser perform its native anchor jump. This keeps the URL free of a changing #chapter-n hash and avoids the native jump firing the IntersectionObserver in an unpredictable order relative to the smooth-scroll animation.

Customizing it

Add a sixth .css-chapter and matching sidebar link with the same data-chapter id — the observer, click handler, and progress rail all work against the live DOM with no fixed chapter count anywhere. On narrow viewports the sidebar hides entirely rather than cramming into a mobile layout, letting the piece read as a plain long-form article. Pair with a scroll reading time estimate at the top, or a scroll spy nav for a shorter, single-page variant of the same active-link idea.

Step by step

How to Use

  1. 1
    Paste the HTML, CSS, and JSNo CDN scripts needed — everything runs on native browser APIs.
  2. 2
    Scroll through the chaptersThe sidebar link for whatever chapter is centered on screen highlights automatically.
  3. 3
    Watch the thin progress railIt fills based on total scroll progress through the whole piece.
  4. 4
    Click a sidebar chapter linkSmooth-scrolls straight to that chapter with no hash jump in the URL.
  5. 5
    Resize to a narrow viewportThe sidebar hides and the piece reads as a plain single-column article.
  6. 6
    Add more chaptersAdd a .css-chapter section plus a matching nav link with the same data-chapter id.

Real-world uses

Common Use Cases

Long-form journalism features
Give readers a persistent sense of position in a long article, using a sticky sidebar that highlights the chapter in the centre of the viewport.
Expedition and research write-ups
Structure travel or research narratives into chapters, with a scroll progress rail and a chapter highlight that track two independent signals.
Book and ebook previews
Present a chapter list alongside the text, built with native IntersectionObserver and no animation library at all.
Technical documentation and long guides
Reuse the same structure for guides that run to thousands of words, where position: sticky keeps the nav pinned only while the article is on screen.
Company history and case studies
Pair with a scroll company timeline for annual report deep dives, or structure a detailed portfolio narrative by chapter.

Got questions?

Frequently Asked Questions

A single IntersectionObserver watches every .css-chapter section with a rootMargin of '-45% 0px -45% 0px', which shrinks its effective detection zone down to a thin horizontal band across the vertical middle of the viewport. Only the chapter section that is genuinely intersecting that middle band is treated as active, so the sidebar highlight always reflects whichever chapter the reader's eyes are actually on, even when chapters vary a lot in length.

They answer two different questions. The chapter highlight, driven by the IntersectionObserver, answers "which chapter is the reader currently in." The thin progress rail is computed separately from window.scrollY divided by total scrollable height, and answers "how far through the entire piece has the reader gotten." A reader midway through a long chapter three would see chapter three highlighted while the rail shows something like 55% — information a single combined indicator couldn't convey as clearly.

position: sticky keeps the sidebar pinned only while its parent grid column (the height of the whole .css-main article) is scrolling past — it naturally stops being sticky and scrolls away once the article ends, with zero JavaScript. A fixed sidebar would stay pinned indefinitely regardless of scroll position, requiring extra show/hide logic to prevent it from floating over content before or after the article, which sticky avoids entirely.

The click handler calls e.preventDefault() and then target.scrollIntoView({ behavior: 'smooth' }) directly, rather than letting the browser perform its native, instant anchor jump. This keeps the page's URL free of a changing #chapter-n hash on every click, and avoids a timing conflict where a native jump could fire the IntersectionObserver's callback in an order that doesn't match the smooth-scroll animation still in progress.

Create the IntersectionObserver and the scroll/resize listeners for the progress rail inside a mount effect (useEffect, onMounted, or ngAfterViewInit) after the chapter sections have rendered, and store the active chapter id in component state rather than toggling DOM classes directly. Disconnect the observer and remove the scroll/resize listeners in the cleanup function to avoid duplicate observers accumulating across re-renders or route changes.