v138 · css

Reading Nav

An article sidebar with section links. scroll-target-group: auto on the nav makes the browser track which anchor is currently in view and apply :target-current to that link — no IntersectionObserver, no scroll event listener. Scroll the article to see the active section highlight update.

Feature detection: checking…
Scroll the article — sidebar tracks current section via :target-current scroll-target-group: auto

Introduction

Scroll-driven sidebar navigation is one of the most common interactive patterns on the web. A sticky sidebar with links to sections of a long article highlights the current section as the reader scrolls — giving them a sense of position and quick access to any part of the document.

Implementing this correctly has historically required JavaScript. Specifically, an IntersectionObserver watching each section heading, with logic to determine which section is "most in view" and toggle an active class on the corresponding nav link.

The Problem

The JavaScript approach has several pain points. First, you need to set up the observer, manage its lifecycle, and handle edge cases like fast scrolling, page load position, and resize events. Second, the "which section is current" heuristic is surprisingly tricky — do you highlight based on first visible, most visible, or top-of-viewport? Different articles need different thresholds.

Third and most importantly: JavaScript is required for what is fundamentally a navigation synchronization concern. The browser already knows exactly where the user is scrolled; it should be able to expose that information to CSS without an observer loop.

Old Approach

The pre-Chrome 138 approach requires roughly 25 lines of JavaScript: create an IntersectionObserver, observe each [id] section, sort by intersection ratio, set a .active class on the winning nav link, and remove it from the others. Clean up the observer on component unmount.

Additionally, the threshold value needs tuning per layout — a sidebar at 50% viewport height needs a different threshold than a sticky header. Some implementations use a sentinel element near the top of each section instead, adding even more DOM.

Chrome 138 Solution

With scroll-target-group: auto on the nav container, the browser automatically tracks which anchor link's target section is currently scrolled into the scroll container. The active link gets the :target-current pseudo-class, which CSS can style directly.

The key insight: HTML anchor links (<a href="#section-id">) already encode the relationship between nav item and section. scroll-target-group tells the browser to use that relationship to determine the "current" link, using the same snap-point / scroll position algorithm it already runs internally.

The CSS

Only two CSS rules are needed to implement the entire pattern:

nav { scroll-target-group: auto; } — marks the nav as a scroll marker group container. All <a href="#…"> descendants become scroll markers.

.nav-link:target-current { font-weight: 700; border-left-color: black; } — styles the currently active link. You can add any visual treatment here: color, background, scale, underline, anything CSS supports.

Compatibility

As of Chrome 138, scroll-target-group and :target-current are available without a flag. Firefox and Safari have not yet shipped the feature. A graceful fallback using the old IntersectionObserver approach is recommended for cross-browser production use — the CSS-only version layers on top via feature detection.

Use CSS.supports('scroll-target-group', 'auto') to detect support, and add the IntersectionObserver fallback as a polyfill when needed.

Pre-138 vs Chrome 138+

Pre-Chrome 138
~25 lines of JavaScript: create IntersectionObserver, observe each section, sort by ratio, toggle .active class on the winning link. Requires threshold tuning, lifecycle management, and resize event handling. Two sources of truth: the scroll position and the JS-managed active class.
Chrome 138+
Two CSS rules: scroll-target-group: auto on the nav, and a :target-current selector for the active style. The browser uses the existing anchor-link relationship to determine "current" — zero JavaScript, zero lifecycle management, no thresholds to tune.
/* scroll-target-group: auto — browser tracks current anchor link */
nav.section-nav {
  scroll-target-group: auto;
}

/* :target-current — styles the link whose section is currently in view */
.nav-link:target-current {
  font-weight: 700;
  border-left-color: var(--text-black);
  background: var(--bg-stone);
  color: var(--text-black);
}

/*
  HTML anchor links () encode the nav→section relationship.
  scroll-target-group tells the browser to resolve :target-current
  based on scroll position, using the same algorithm as scroll markers.

  No IntersectionObserver. No event listeners. No threshold tuning.
*/

see also