Scroll Spy
createScrollSpy(targets, options, onChange) — tracks which of a list of targets currently sits in an "active" band near the viewport's vertical center, and reports its index — the standard nav-highlight idiom (a docs sidebar, a table of contents) without hand-rolling scroll math. Built entirely on the pooled visibility engine, so every target sharing the same rootMargin/threshold/root (the common case — they all come from one call) shares a single native IntersectionObserver.
function createScrollSpy(
targets: Array<Element | null | undefined>,
options: ScrollSpyOptions | undefined,
onChange: (activeIndex: number | null) => void,
): ScrollSpy
interface ScrollSpy {
destroy(): void
}import { createScrollSpy } from '@macrulez/inview-core'
const sections = document.querySelectorAll('section[id]')
const spy = createScrollSpy([...sections], undefined, (activeIndex) => {
const active = activeIndex === null ? null : sections[activeIndex]
navLinks.forEach((link, i) => link.classList.toggle('active', i === activeIndex))
})
// later
spy.destroy()Options
ScrollSpyOptions
| Field | Type | Default |
|---|---|---|
rootMargin | string | '-45% 0px -45% 0px' — a thin band near the vertical center; the boundary that crosses it decides the active target |
threshold | number | number[] | — |
root | Element | null | null |
pool | ObserverPool | shared default pool |
Tune rootMargin (and threshold) per layout — a page with very short sections might want a narrower band than the default.
onChange(activeIndex)
Called with the index into targets of whichever one currently sits in the band, or null when none does. When more than one target is simultaneously in the band (short sections, a narrow band), the earliest one in targets wins — deterministic, and matches reading order.
Return value
destroy()
() => void
Unobserves every target and tears down the underlying engine.
Reactive version: see React Hooks for React; the Vue composable is below.
useScrollSpy (Vue)
function useScrollSpy(
targets: MaybeRefOrGetter<Array<HTMLElement | null | undefined>>,
options?: UseScrollSpyOptions,
): Ref<number | null>UseScrollSpyOptions is the same shape as ScrollSpyOptions above. Re-subscribes whenever targets itself changes (as a ref/getter); options are captured at that point, the same one-shot model useRevealController has, not a reactive ref/getter of its own.
<script setup lang="ts">
import { useTemplateRefs } from 'vue' // or collect refs manually in a v-for
import { useScrollSpy } from '@macrulez/inview-vue'
const sections = ref<HTMLElement[]>([])
const activeIndex = useScrollSpy(sections)
</script>
<template>
<nav>
<a v-for="(item, i) in items" :key="item.id" :class="{ active: i === activeIndex }">
{{ item.title }}
</a>
</nav>
</template>