Santaji GadeJavaScript2 days ago8 Views

A sticky table of contents using CSS & JavaScript combines position: sticky with IntersectionObserver — plus a look at the emerging CSS-only method.
Table of Contents
ToggleA 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.
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.
Here's the minimal CSS to pin a table of contents sidebar in place while the rest of the page scrolls past it.
/* 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; }
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.
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.
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.
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.
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');
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.
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; } }
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.
A quick reference for choosing between the traditional and emerging approaches.
| Approach | Browser Support | Best For |
|---|---|---|
| CSS position: sticky + IntersectionObserver | All modern browsers | Production sites needing full compatibility |
| CSS scroll-target-group (2026) | Chrome 140+ only, experimental | Progressive enhancement, cutting-edge projects |
| Background-patch CSS trick | All modern browsers | Simple highlighting without JS, less precise |
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.
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.
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
A sticky table of contents works best paired with auto-generated headings and scroll highlighting. Explore both guides next.









