Sticky Table of Contents Using CSS & JavaScript: Complete Guide

Santaji GadeJavaScript2 days ago8 Views

Sticky Table of Contents

A sticky table of contents using CSS & JavaScript combines position: sticky with IntersectionObserver — plus a look at the emerging CSS-only method.

Technical SEO Sticky TOC CSS & JavaScript 2026

A sticky table of contents using CSS and JavaScript combines two separate jobs: CSS's position: sticky keeps the navigation pinned in view while scrolling, and JavaScript's IntersectionObserver highlights which section is currently active. Neither piece alone gives the full experience readers expect from documentation sites and long-form articles.

01Sticky Table of Contents Using CSS: The Foundation

Position sticky behaves like a relatively positioned element until a specific scroll threshold is reached, after which it locks into place like position: fixed, but only within the bounds of its parent container.

Polypane's guide is refreshingly honest about the catch: sticky can be surprisingly tricky to work with because it comes with real constraints, and you can end up in a situation where an element should be stuck, but it just silently isn't. W3Tweaks' guide confirms the same experience: add position: sticky and top: 0 to a header and nothing happens, no error, it just silently refuses to stick.

0
JavaScript lines needed just to make the TOC stick, sticky positioning is pure CSS
3
common traps that silently break position: sticky without any console error
2026
year an experimental CSS-only scrollspy method (scroll-target-group) began shipping
Advertisement
Advertisement

02The Basic Sticky CSS

Here's the minimal CSS to pin a table of contents sidebar in place while the rest of the page scrolls past it.

Basic Sticky Sidebar CSS
/* The sidebar sticks once it reaches 24px from the top */
.toc-sidebar {
  position: sticky;
  top: 24px;
  align-self: start;
  max-height: calc(100vh - 48px);
  overflow-y: auto;
}

.article-layout {
  display: grid;
  grid-template-columns: 1fr 260px;
  gap: 40px;
  align-items: start;
}
Quick Tip

The parent must not have overflow: hidden, and the sticky element needs a defined top value to work at all. Both are silent failures, position: sticky won't throw an error, it just quietly behaves like position: static instead.

Advertisement
Advertisement

03Handling a TOC Taller Than the Viewport

A long table of contents can exceed the visible screen height, getting clipped instead of scrolling with the page. Setting max-height and overflow-y: auto on the sticky container, as shown above, lets the TOC itself scroll internally once it runs out of vertical room.

🔎 Did you know?

Mastery Games' explainer puts it memorably: the element acts like a sleeper agent, behaving exactly like position: relative and fooling its own parent and sibling elements, right up until the scroll threshold is met, at which point it activates fixed-style behavior.

04Adding JavaScript Scrollspy Highlighting

This is where the IntersectionObserver pattern covered in our scrollspy guide plugs directly into a sticky sidebar, watching each heading and toggling an active class on the matching TOC link.

Scrollspy Highlighting for a Sticky TOC (JavaScript)
function highlightStickyToc(contentSelector, tocSelector) {
  const headings = document.querySelectorAll(`${contentSelector} h2, ${contentSelector} h3`);
  const tocLinks = document.querySelectorAll(`${tocSelector} a`);

  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        tocLinks.forEach(link => link.classList.remove('active'));
        const activeLink = document.querySelector(
          `${tocSelector} a[href="#${entry.target.id}"]`
        );
        if (activeLink) activeLink.classList.add('active');
      }
    });
  }, { rootMargin: '0px 0px -70% 0px' });

  headings.forEach(heading => observer.observe(heading));
}

highlightStickyToc('.article-content', '.toc-sidebar');

05The New CSS-Only Approach (2026)

Una Kravets' writeup covers a genuinely new option: the scroll-target-group CSS property, paired with the :target-current pseudo-class, lets the browser handle active-link tracking natively, no JavaScript, no IntersectionObserver, no scroll listeners at all.

Experimental CSS-Only Scrollspy (Chrome 140+)
ol.toc-sidebar {
  scroll-target-group: auto;
}

.toc-sidebar a:target-current {
  color: #4a9e24;
  font-weight: 700;
}

/* Feature-detect, since browser support is still limited */
@supports (scroll-target-group: auto) {
  .toc-sidebar ol { scroll-target-group: auto; }
}
Quick Tip

This feature is only available in the latest Chromium browsers as of 2026, so always wrap it in an @supports feature query and keep your JavaScript IntersectionObserver version as a fallback for Firefox and Safari users.

Advertisement
Advertisement

06Approach Comparison

A quick reference for choosing between the traditional and emerging approaches.

ApproachBrowser SupportBest For
CSS position: sticky + IntersectionObserverAll modern browsersProduction sites needing full compatibility
CSS scroll-target-group (2026)Chrome 140+ only, experimentalProgressive enhancement, cutting-edge projects
Background-patch CSS trickAll modern browsersSimple highlighting without JS, less precise

07Implementation Checklist

A short list to confirm before shipping a sticky table of contents to production.

Always set an explicit top value, sticky silently fails to activate without one.

Check every ancestor for overflow: hidden, a single parent with it breaks sticky entirely.

Cap TOC height with max-height and overflow-y: auto, so tall lists scroll internally instead of clipping.

Pair sticky CSS with IntersectionObserver, sticky alone only handles position, not active highlighting.

Feature-detect scroll-target-group, if experimenting with the CSS-only approach, keep a JS fallback ready.

08Common Questions

The two most common causes: no top value is set, or a parent container has overflow: hidden. Both cause sticky to silently behave like position: static with no console warning.

For the sticking behavior alone, no. Position: sticky is pure CSS. JavaScript, via IntersectionObserver, is only needed to highlight which section is currently active as the reader scrolls.

Without handling this, the TOC gets clipped at the viewport edge. Setting max-height and overflow-y: auto on the sticky container lets it scroll internally instead.

Not yet broadly. As of 2026, scroll-target-group only works in the latest Chromium browsers. Use it as progressive enhancement behind an @supports check, with JavaScript as the fallback.

Fixed positions relative to the viewport at all times. Sticky behaves like relative positioning until a scroll threshold is crossed, then switches to fixed-like behavior, but only within its parent container's bounds.

What We Learn Today

Position: sticky needs no JavaScript, just an explicit top value

overflow: hidden on any ancestor silently breaks sticky

max-height and overflow-y handle TOCs taller than the viewport

IntersectionObserver adds active-link highlighting on top of sticky

scroll-target-group offers a CSS-only alternative in modern Chrome

Always feature-detect experimental CSS with @supports

Build a Complete Content Navigation System

A sticky table of contents works best paired with auto-generated headings and scroll highlighting. Explore both guides next.

0 Votes: 0 Upvotes, 0 Downvotes (0 Points)

Leave a reply

Loading Next Post...
Search
Popular Now
Loading

Signing-in 3 seconds...

Signing-up 3 seconds...