Pane root
Observe a scroll container via data-scrollspy-root / React
root — see Preview.
Best for embedded docs demos and side-by-side layout.
Lightweight scrollspy — IntersectionObserver highlights nav links for the section in view (window or a scroll container).
Live
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.
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.
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.
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>
import { Scrollspy } from 'caustica-design/core'
<Scrollspy root={paneEl} rootMargin="0px 0px -55% 0px">
<nav className="ui-scrollspy__nav" aria-label="Sections">
<a href="#one">One</a>
<a href="#two">Two</a>
</nav>
</Scrollspy>
{/* Sections live in the document or inside the scroll root */}
<div className="ui-scrollspy__pane" ref={setPaneEl} id="pane">
<section className="ui-scrollspy__section" id="one">…</section>
<section className="ui-scrollspy__section" id="two">…</section>
</div>
Observe a scroll container via data-scrollspy-root / React
root — see Preview.
Best for embedded docs demos and side-by-side layout.
Omit the root (or pass null) to spy against the window.
Use for long marketing or docs pages with a sticky side nav.
Matching hash gets .is-active and
aria-current="location".
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.
aria-current="location" (section-in-view, not page).
aria-label.tabindex="0" so keyboard users can focus and
scroll the region.
| 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. |
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. |