Scrollspy

Automatically update navigation to reflect the section currently in view. Ship a ready nav with ScrollspyNav, or wire your own with the useScrollspy hook — both use an IntersectionObserver under the hood.

Import

tsx
import { ScrollspyNav, useScrollspy } from '@metatoy/bootstrap-styled';

Live scrollspy

Give ScrollspyNav a list of items — each an { id, label } pointing at a section — and a root scroll container. As you scroll, the link for the section anchored near the top of the region becomes active:

By default the observer watches the viewport; pass root (an element or ref) to spy on a specific scroll container, as this demo does. Fine-tune activation with rootMargin — the default matches Bootstrap 5.3.8’s 0px 0px -25%.

List-group variant

Set variant="list-group" to render the spy as Bootstrap list-group items instead of nav links. Setting activeId pins the active item statically (the observer is skipped), which is handy for a fixed table of contents:

The hook

For full control over markup, drive your own nav with useScrollspy. It returns the id of the section currently in view, which you compare against to set active state:

tsx
import { useScrollspy } from '@metatoy/bootstrap-styled';

function Toc() {
const active = useScrollspy({ ids: ['intro', 'usage', 'api'] });
return (
  <nav>
    <a href="#intro" aria-current={active === 'intro' ? 'true' : undefined}>Intro</a>
    <a href="#usage" aria-current={active === 'usage' ? 'true' : undefined}>Usage</a>
    <a href="#api"   aria-current={active === 'api'   ? 'true' : undefined}>API</a>
  </nav>
);
}

Props

PropTypeDefaultDescription
itemsScrollspyItem[]
activeIdstringControlled/static active id. When set, the observer is not used.
variant'nav' | 'list-group'Nav look — Bootstrap `.nav` links or `.list-group` items.
rootElement | null
rootMarginstring
onActiveChange(id: string) => void

ScrollspyNav also accepts onActiveChange(id) to react to changes, and root / rootMargin to configure the observer. The useScrollspy hook takes { ids, root, rootMargin, threshold } and returns the active id.

Theming

In nav mode the links use --bs-nav-link-color / --bs-nav-link-hover-color and the active pill uses --bs-nav-pills-link-active-bg / --bs-nav-pills-link-active-color (with --bs-nav-pills-border-radius). In list-group mode it reads the --bs-list-group-* tokens — --bs-list-group-active-bg, -active-color, -action-hover-bg, and the border/padding tokens. Both modes pick up your theme’s --bs-primary for the active accent, so a single color override re-skins the highlight.

Accessibility

Scrollspy is a progressive enhancement over in-page anchor links: the nav still works as plain links if the observer never runs. The active link is marked so assistive tech and styling can reflect the current section — pair it with a visible focus style and keep the links keyboard-reachable. Because the highlight follows scroll position rather than focus, ensure the underlying anchors remain fully operable on their own.

Explore in Ladle

Open the Scrollspy stories in Ladle ↗