Overview
Installation
Usage
Watch the blue bar in the “On this page” list move itself as the article scrolls. Click a link to jump to that section.
Scrollspy
/ IntersectionObserver · aria-current="location" /
also called scroll-linked navigation, current-section navigation, active table of contents, On this page navigation, in-page navigation, table of contents highlighting
“The contents list down the side that follows where I am in the article” is a scrollspy, also called scroll-linked or in-page navigation. The rail is a plain <nav> of fragment links (href="#section-id" pointing at real heading ids); an IntersectionObserver watches those section targets and moves aria-current="location" — plus the visible accent — to the one link for the section on screen. It looks like tabs, but it isn't: tabs swap one shared panel when you activate a tab, while a scrollspy leaves the whole document in place and only reflects where you already are. Sticky positioning is what keeps the rail beside the article; scrollspy is what changes its current item.
If you called it…
…you meant a scrollspy.
Anatomy — every part, named
- 1On-this-page rail
<nav>“The little contents list down the side that follows where I am in the article” is the On-this-page rail — a labeled <nav> of same-document fragment links, usually kept in view with position: sticky.
- 2Current-section indicator
aria-current="location"“The colored line or bold link that jumps down the contents list as I scroll” is the current-section indicator — one link at a time carries aria-current="location" and the accent.
- 3Section anchor target
IntersectionObserver.observe()“The headings the side list is watching to know which one to light up” are the section anchor targets — real headings whose id values match the rail's href fragments.
Prompt — paste into your agent
Build Scrollspy (scroll-linked navigation) for this article: a labeled <nav> "On this page" rail of same-document fragment links kept beside the content with position: sticky, and an IntersectionObserver watching each section anchor target so exactly one link carries aria-current="location". Make the current-section indicator unmistakable — a high-contrast vertical accent bar plus a stronger label that move to the section in view — and use rootMargin to offset the activation zone by the sticky header height.
Debug prompt — when it misbehaves
Paste this, then describe what you’re seeing — it hands your agent the classic failure modes to rule out first.
Debug my scrollspy / On-this-page navigation (IntersectionObserver, aria-current="location", fragment links). Rule out: the highlight changing too early or too late because rootMargin does not account for the sticky header height; two or more links marked current at once because the observer callback sets state per entry instead of picking one winner from all intersecting sections; the last section never becoming current because the page cannot scroll far enough to bring it into the activation zone (pad the end of the article or fall back to a bottom-of-scroll rule); nothing highlighting at all because the observer root is the viewport while the sections scroll inside an overflow container, or because observe() ran before the sections existed; the rail links given role="tab" and aria-selected instead of aria-current, which makes screen readers announce a tab widget that does not exist; clicking a link jumping correctly but the highlight snapping back because the scroll-triggered observer overrides the clicked section. The symptom:
In code
The exact names this thing goes by in code — each row is one framework’s word for it. Use the row that matches your project (or paste it into your prompt).
| Web API | IntersectionObserver | watches which section target is in view |
| ARIA | aria-current="location" | marks the ONE link for the section being read |
| Bootstrap | bootstrap.ScrollSpy | the ready-made implementation the name comes from |
| Bootstrap | data-bs-spy="scroll" | declarative initializer, paired with data-bs-target |
| HTML | <nav> | the labeled landmark holding the On this page links |
| Web API | IntersectionObserver.rootMargin | shifts the activation zone under a sticky header |