Skip to content

Components

A Scroll-Spy Table of Contents Without a Scroll Listener

IntersectionObserver Does the Math the Browser Was Already Doing
Written by Aiden Wood Founder and Lead Design Engineer
📑 👁️ 🧭

The sticky sidebar TOC with the highlighted current section is table stakes for docs and long reads. The common build is a scroll listener comparing getBoundingClientRect() for every heading on every scroll frame — layout thrash, on the main thread, for information the browser already has.

Velocity X's table-of-contents does it the free way: one IntersectionObserver watching the headings, zero scroll listeners, zero per-frame work.

The Money Pattern

useEffect(() => {
  const headings = document.querySelectorAll('article h2, article h3');
  const observer = new IntersectionObserver(
    (entries) => {
      for (const e of entries) {
        if (e.isIntersecting) setActive(e.target.id);
      }
    },
    // The magic: a band near the top of the viewport, not the whole screen
    { rootMargin: '-10% 0px -80% 0px' }
  );
  headings.forEach((h) => observer.observe(h));
  return () => observer.disconnect();
}, []);

That rootMargin is the whole trick. It shrinks the observation zone to a thin band 10–20% from the viewport top, so "intersecting" means "this heading just became the one you're reading" — not "this heading is somewhere on screen". Without it, three sections register at once and the highlight jitters.

The Catch

Short final sections break the naive version: a 200px conclusion never reaches the band, so the TOC never highlights it even at full scroll. The fix is one special case — at scroll bottom, force the last heading active.

Accessibility is the other skipped step. The TOC is navigation, so it's a <nav aria-label="On this page">, and the active link gets aria-current="true" — not just a colour change, which screen readers can't see and colour-blind users can barely see.

The Verdict

Scroll listeners doing rect math is 2015 code. One observer, one rootMargin band, one bottom-of-page special case — smoother highlight, zero main-thread cost. The finished version sits in Velocity X's Long-form category with the pull quotes, reading-time and callout components that make article pages feel finished.

Let us make some quick suggestions?

Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.