Component
Scrollspy
Published on
Description
A scrollspy highlights the navigation link for the section currently in view, giving the reader a persistent sense of place within a long document. It wraps a table of contents whose links are in-page anchors; as the reader scrolls, the link for the section in the top half of the viewport is marked as current.
The pattern is progressive: with no scripting the navigation is still a plain list of working jump links. The ds-scrollspy web component lazy-loads and layers the active highlighting on top using an IntersectionObserver.
This is a transitional implementation. The vanilla styles already set scroll-target-group: auto on the navigation and highlight the active link with the native :target-current selector. The JavaScript component only defines itself when CSS.supports("scroll-target-group", "auto") is false, so it removes itself automatically once native CSS scroll-target-group reaches baseline support.
Example
Usage
Wrap the table of contents in <nav is="ds-scrollspy">. Each <a href="#id"> points at the id of a section heading, and every target section needs a matching <h2 id> so the observer can track it. The observer looks up section:has(h2[id]) first, then falls back to any element carrying that id. A page opened at a #hash, and later hashchange events, set the active link too.
<nav is="ds-scrollspy" aria-labelledby="toc-label">
<section>
<header><h2 id="toc-label">On this page</h2></header>
<ol>
<li><a href="#introduction">Introduction</a></li>
<li><a href="#usage">Usage</a></li>
<li><a href="#accessibility">Accessibility</a></li>
</ol>
</section>
</nav>
<section>
<h2 id="introduction">Introduction</h2>
</section>
<section>
<h2 id="usage">Usage</h2>
</section>
<section>
<h2 id="accessibility">Accessibility</h2>
</section> In Svelte, use NavScrollspy with a children snippet holding the <Ol> of links. It accepts labelHeader (the heading text, default On this page
), labelId, and is (default ds-scrollspy).
Variants
Two implementations of the same ds-scrollspy element ship with the design system. Both register the same custom element name, so the behaviour is chosen by loading one implementation or the other, not by changing the is attribute.
ds-scrollspy— the default. It tracks every section independently, so more than one link can be current at once when several short sections share the top half of the viewport. Best for a long table of contents.ds-scrollspy-single— keeps exactly one link current at a time, choosing the nearest heading from the scroll direction. Prefer it when a single active item reads more clearly, such as a short list.
Accessibility
- The active link receives
aria-current="true"and it is removed from the others, so assistive technology announces the section currently in view. - The navigation is labelled with
aria-labelledbypointing at itsOn this page
heading, making it identifiable in a list of landmarks. - Links are real in-page anchors, so they are keyboard focusable and operable with or without the script — the highlight is an enhancement, never a requirement for navigation.
- The default variant can mark several links current at once; prefer
ds-scrollspy-singlewhen a single announced location is less verbose.
References
- Pure CSS scroll-driven Scrollspy(opens in new tab) — Sara Soueidan
- Table of Contents in Long-Form Content(opens in new tab) — Nielsen Norman Group
- Can I use: scroll-target-group(opens in new tab) — baseline tracking for the native replacement
Related
- Documentation — the two column layout that hosts the scrollspy in its aside
- Links — the underlying in-page anchor pattern
Variables
labelHeaderlabelIdis