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
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:
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
| Prop | Type | Default | Description |
|---|---|---|---|
| items● | ScrollspyItem[] | — | — |
| activeId | string | — | Controlled/static active id. When set, the observer is not used. |
| variant | 'nav' | 'list-group' | — | Nav look — Bootstrap `.nav` links or `.list-group` items. |
| root | Element | null | — | — |
| rootMargin | string | — | — |
| 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.