Scrollspy
Scrollspy automatically highlights the nav link for whichever section of a long page the user has currently scrolled to. We'll use it on TrailMap's trail-guide page — a long single-page write-up with Overview, Difficulty, Directions, and Reviews sections — paired with a sticky side-nav that tracks the reader's position.
What Scrollspy Does
Without it, a page with jump-link navigation only updates which link looks "active" when
the user clicks one — scrolling past a section manually does nothing to the nav. Scrollspy
watches scroll position and adds .active to the matching nav link the moment
its target section comes into view, with no clicking required.
data-bs-spy="scroll" and data-bs-target
Two attributes go on the scrollable container — the element the user actually
scrolls inside (this can be the whole page, or a smaller scrollable <div>):
| Attribute | Role |
|---|---|
data-bs-spy="scroll" | Turns on scroll tracking for this container. |
data-bs-target | Points (by #id) at the nav element whose links should get highlighted. |
<body data-bs-spy="scroll" data-bs-target="#guideNav" tabindex="0">
The nav itself needs each link's href to match a matching section's
id — Scrollspy scans for those ids to know what to watch:
<nav id="guideNav" class="nav flex-column">
<a class="nav-link" href="#overview">Overview</a>
<a class="nav-link" href="#difficulty">Difficulty</a>
<a class="nav-link" href="#directions">Directions</a>
<a class="nav-link" href="#reviews">Reviews</a>
</nav>
<body>), no extra CSS is needed. When it's a smaller <div> instead, that div needs its own overflow-y: scroll and a fixed height for Scrollspy to have something to track.Worked Example: TrailMap's Trail-Guide Page
The Ridgeline Loop trail guide is long enough to need its own in-page navigation. A sticky
side-nav sits alongside the write-up, its links matching the four section
ids, and the current section highlights itself as the reader scrolls:
<body data-bs-spy="scroll" data-bs-target="#guideNav" data-bs-offset="80" tabindex="0">
<div class="row">
<div class="col-md-3">
<nav id="guideNav" class="nav flex-column sticky-top" style="top: 20px;">
<a class="nav-link" href="#overview">Overview</a>
<a class="nav-link" href="#difficulty">Difficulty</a>
<a class="nav-link" href="#directions">Directions</a>
<a class="nav-link" href="#reviews">Reviews</a>
</nav>
</div>
<div class="col-md-9">
<h2 id="overview">Overview</h2>
<p>A 7.2 mile loop through mixed forest with a summit overlook near the midpoint…</p>
<h2 id="difficulty">Difficulty</h2>
<p>Rated Moderate: 1,400 ft of elevation gain, mostly on maintained switchbacks…</p>
<h2 id="directions">Directions</h2>
<p>From the visitor center, follow Forest Road 12 for 3 miles to the trailhead lot…</p>
<h2 id="reviews">Reviews</h2>
<p>"Great early-morning hike, minimal crowds before 9am." – verified hiker</p>
</div>
</div>
</body>
As the reader scrolls from "Overview" down through "Reviews," Scrollspy adds
.active to each matching link in #guideNav in turn and removes
it from the previous one — the data-bs-offset="80" above accounts for a fixed
80px site header so a section counts as "current" slightly before it reaches the very top
of the viewport.
Refreshing After Content Changes
If TrailMap loads extra guide sections dynamically after the page renders (say, more reviews fetched in the background), Scrollspy needs to be told to re-scan for new targets — it doesn't watch the DOM for changes on its own:
const spyEl = document.querySelector('body');
const scrollSpy = bootstrap.ScrollSpy.getInstance(spyEl);
scrollSpy.refresh();