Skip to content

Scroll spy ​

useScrollSpy() — which section is in view, from an IntersectionObserver over ids or a selector.

Basic
News — scroll the page; the nav follows.
Story — scroll the page; the nav follows.
Cast — scroll the page; the nav follows.
ts
const { active } = useScrollSpy(ids | selector, { rootMargin?, root? });
// ids: string[]           — element ids to observe
// selector: string        — or every element matching it
// rootMargin: string      — default "0px 0px -60% 0px"
// root: Ref<HTMLElement>  — a scroll container instead of the viewport
// active: Ref<string | undefined>

active is the id of the intersecting section whose top edge is nearest the top of the viewport (or of root, when you pass a scroll container) — measured live on every change, so a long section that has just scrolled past doesn't outrank the one arriving. With the default rootMargin only the top 40% of the viewport counts, so a section becomes active as it reaches the upper part of the screen rather than the moment it enters at the bottom. When nothing intersects (above the first section, or in a gap) active keeps its last value; before any scroll it is the first observed id.

Pair it with BlessSidebarNav :active as above, and give sections scroll-margin-top equal to whatever sticks at the top, so a click doesn't land under the bar. The observer is created on mount, so the composable is safe to call in server-rendered components. It observes the targets that exist at that moment: sections rendered later (behind a v-if, loaded after a fetch) are not picked up — put a :key on the component that calls it, tied to the list, so it re-runs when the sections change.

Released under the MIT License. Character names and artwork belong to their owners.