Component

Scrollspy

Published on

Description Link to section 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 Link to section example

Open in new tab(opens in new tab)
View HTML markup
<nav aria-labelledby="nav-scrollspy-label" is="ds-scrollspy">
  <section>
    <header>
      <h2 id="nav-scrollspy-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>
  <p>Scroll the frame to see the matching link highlight as each section reaches the top half of the viewport.</p>
</section>
<section>
  <h2 id="usage">Usage</h2>
  <p>Each link points to the id of a section heading. The active link is marked as you read down the page.</p>
</section>
<section>
  <h2 id="accessibility">Accessibility</h2>
  <p>The link for the section in view receives aria-current="true" so assistive technology announces the current location.</p>
</section>

Usage Link to section 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).

Accessibility Link to section 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-labelledby pointing at its On 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.
  • Several links can be current at once when short sections share the top half of the viewport.

References Link to section references

  • Documentation — the two column layout that hosts the scrollspy in its aside
  • Links — the underlying in-page anchor pattern

Variables Link to section variables

  • labelHeader
  • labelId
  • is