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: autoto the nav list container - Use
:target-currentto highlight the active section's link - Use
:target-beforeand:target-afterfor additional states - Content must scroll on the document, not a nested container
Happy scrolling!