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>):

AttributeRole
data-bs-spy="scroll"Turns on scroll tracking for this container.
data-bs-targetPoints (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>
Note: when the scrollable container is the whole page (as above, on <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();
Next: Head to Form Controls to start the Forms section of this track.