demo · v138

Dynamic table of contents

Scroll the article. The sidebar marks the active section. scroll-target-group: auto tells the browser to treat every focusable child as a member of one shared scroll-target group — meaning JS doesn’t have to wire each link to an IntersectionObserver any more.

checking support…

Introduction

Scroll-target-group reframes a familiar pattern. Every TOC widget on the web does roughly the same thing: it watches the visible section and highlights the corresponding link. That used to mean an IntersectionObserver per link, a debounce, and a lot of subtle bugs around what “currently visible” means when two sections share the viewport.

What came before

The early-2010s approach was a scroll listener and a hand-rolled binary search. The mid-2020s approach was an IntersectionObserver and a Set. Both worked but both turned tiny intent (“show me where I am”) into surprisingly verbose JS.

What scroll-target-group does

Sets up a single scrollable region as the source of truth for which member is currently focal, and lets selectors (:target-current and friends) hook into that. JS becomes optional — the browser tracks the visible target and the CSS adapts.

Implementation in this demo

This page uses a lightweight IO fallback to mark the active TOC entry, because the full :target-current selector is still rolling out. The structural setup — one container with scroll-target-group: auto, one scroll-snap-align: start per section — is the long-term shape.

Edge cases

What happens at the bottom of the scroller when the last section is shorter than the viewport? scroll-target-group resolves it by preferring the “most-visible” element in the group, with deterministic tie-breaking. No more glitching between two sections at a snap boundary.

What’s coming next

The same group concept extends to cross-document view-transitions and to scroll-driven animations. Treating a set of elements as “one logical thing” lets the browser orchestrate them as a unit.

the property

.article {
  scroll-target-group: auto;
  /* Every scroll-snap child becomes a member of the implicit group.
     :target-current and similar pseudo-classes then identify the
     currently focal target, without per-link JS. */
}

see also