CSS-Only Scroll-Driven Table of Contents

Learn how to build a table of contents that highlights the current section using only CSS—no JavaScript required.

Introduction

For years, creating a table of contents that highlights the current section as the user scrolls required JavaScript. Libraries like ScrollSpy from Bootstrap or custom Intersection Observer implementations were the go-to solutions.

But CSS has evolved. With the introduction of scroll-target-group and the :target-current, :target-before, and :target-after pseudo-classes, we can now achieve this entirely with CSS.

This article itself is a live demonstration—look at the sidebar on the left. As you scroll through these sections, the corresponding link in the table of contents highlights automatically.

How It Works

The CSS scroll-driven TOC relies on three key features working together: a property to enable scroll tracking, and pseudo-classes to style links based on their target's scroll position.

scroll-target-group

The scroll-target-group property is applied to the container of your navigation links (like a <ul> or <ol>). It converts anchor links inside into "scroll markers" that the browser tracks.

.toc-list {
  scroll-target-group: auto;
}

The value is auto to enable tracking, or none (the default) to disable it. When enabled, anchor links inside this container become scroll markers.

:target-current

The :target-current pseudo-class matches a link whose href points to an element that is currently the active scroll target.

.toc-link:target-current {
  color: rgb(34 211 238); /* cyan-400 */
}

When the user scrolls to a section, the browser determines which tracked element is "current" (roughly, which one occupies the most viewport space or crosses a threshold). Any link pointing to that element will match :target-current.

:target-before & :target-after

In addition to :target-current, you get two more pseudo-classes for styling links based on scroll position:

  • :target-before — matches links to sections the user has already scrolled past
  • :target-after — matches links to sections the user hasn't reached yet
/* Sections already read */
.toc-link:target-before {
  color: rgb(161 161 170); /* zinc-400 */
}

/* Sections not yet reached */
.toc-link:target-after {
  color: rgb(82 82 91); /* zinc-600 */
}

This allows for sophisticated visual feedback. For example, you might show "read" sections in a muted color, the current section highlighted, and upcoming sections slightly dimmed.

Implementation

Let's look at how to implement this in practice. The setup requires careful attention to your HTML structure and a few lines of CSS.

HTML Structure

The key is having a list container with scroll-target-group: auto containing links that point to section IDs:

<nav>
  <ul class="toc-list">
    <li>
      <a href="#intro" class="toc-link">Introduction</a>
    </li>
    <li>
      <a href="#details" class="toc-link">Details</a>
      <ul>
        <li>
          <a href="#sub-topic" class="toc-link">Sub Topic</a>
        </li>
      </ul>
    </li>
  </ul>
</nav>

<main>
  <section id="intro">...</section>
  <section id="details">
    <section id="sub-topic">...</section>
  </section>
</main>

Important: The content must scroll on the main document (not a nested scroll container) for the tracking to work correctly.

CSS Styles

Here's the complete CSS needed for the scroll-driven effect:

/* Enable scroll tracking on the list */
.toc-list {
  scroll-target-group: auto;
}

/* Current section - highlighted */
.toc-link:target-current {
  color: rgb(34 211 238); /* cyan */
}

/* Passed sections */
.toc-link:target-before {
  color: rgb(161 161 170); /* muted */
}

/* Upcoming sections */
.toc-link:target-after {
  color: rgb(82 82 91); /* dim */
}

That's the core of it. You can add transitions, indicators, and other visual enhancements as needed.

Browser Support

These features are part of the CSS Overflow Module Level 5 specification. Browser support as of early 2026:

  • Chrome/Edge: Supported since version 140
  • Firefox: In development
  • Safari: In development

For production use today, you might want to include a JavaScript fallback for browsers that don't support these features yet. You can detect support with:

@supports (scroll-target-group: auto) {
  /* Use CSS-only approach */
}

@supports not (scroll-target-group: auto) {
  /* Fallback styles for JS-based solution */
}

As with all cutting-edge CSS, progressive enhancement is key.

Conclusion

The scroll-target-group property and its companion pseudo-classes represent a significant step forward for CSS. Patterns that once required JavaScript are becoming native browser features.

This follows the trend we've seen with CSS scroll-snap, container queries, and the :has() selector—the platform is becoming more powerful, reducing our reliance on JavaScript for presentational concerns.

Key takeaways:

  • Apply scroll-target-group: auto to the nav list container
  • Use :target-current to highlight the active section's link
  • Use :target-before and :target-after for additional states
  • Content must scroll on the document, not a nested container

Happy scrolling!