Scrollspy

Lightweight scrollspy — IntersectionObserver highlights nav links for the section in view (window or a scroll container).

Preview

Live

Overview

Scrollspy watches section ids with IntersectionObserver and marks the matching nav link with .is-active and aria-current="location". Scroll this pane to see the side nav update.

Setup

Wrap a nav of hash links in .ui-scrollspy. Point data-scrollspy-root (or the React root prop) at a scroll container when you are not using the viewport.

Usage

Optional rootMargin and threshold tune which band of the root counts as “in view.” Defaults favor the upper portion of the pane so the active link tracks as you read downward.

Accessibility

The active link exposes aria-current="location". Keep section headings clear and give the nav an accessible name. Prefer in-page anchors that match real landmark ids.

<div class="ui-scrollspy" data-scrollspy data-scrollspy-root="#pane">
  <nav class="ui-scrollspy__nav" aria-label="Sections">
    <a href="#one">One</a>
    <a href="#two">Two</a>
  </nav>
  <div class="ui-scrollspy__pane" id="pane">
    <section class="ui-scrollspy__section" id="one">…</section>
    <section class="ui-scrollspy__section" id="two">…</section>
  </div>
</div>

Variants

Pane root

Observe a scroll container via data-scrollspy-root / React root — see Preview.

Best for embedded docs demos and side-by-side layout.

Viewport root

Omit the root (or pass null) to spy against the window.

Use for long marketing or docs pages with a sticky side nav.

Active link

Matching hash gets .is-active and aria-current="location".

Tuned band

Default rootMargin is 0px 0px -55% 0px so the upper ~45% of the root drives the active section.

Override with data-root-margin or React props as needed.

Accessibility

  • Active link uses aria-current="location" (section-in-view, not page).
  • Give the side nav an accessible name via aria-label.
  • Section targets need stable ids that match the hash links.
  • Prefer real headings inside sections so the scroll content stays understandable without the spy.
  • When using a pane, consider tabindex="0" so keyboard users can focus and scroll the region.

API

HTML classes

Class Description
.ui-scrollspy Root grid (nav + pane).
.ui-scrollspy__nav Sticky side nav of hash links.
.ui-scrollspy__pane Optional scroll container for sections.
.ui-scrollspy__section Observed section block inside the pane.
.is-active Highlighted link for the section in view.

React props

From src/core/Scrollspy.tsx — wrap a nav of a[href="#id"] links; matching sections are observed in the document or inside root.

Prop Type Default Description
root Element | null null Scroll container for observation; omit for the viewport.
rootMargin string '0px 0px -55% 0px' IntersectionObserver rootMargin (active band).
threshold number | number[] 0 IntersectionObserver threshold.
onActiveChange (sectionId: string | null) => void Fired when the highlighted section id changes.
className string Merged via cx onto .ui-scrollspy.