Scrollspy

Update navigation based on scroll position inside a target container.

How it works

Scrollspy observes a scroll container and toggles .active on links whose href matches visible target ids. Each target must exist, be visible, and have enough scrollable spacing for Bootstrap to calculate a meaningful active section.

Scrollspy uses Intersection Observer in Bootstrap 5.3. Use a focusable scroll area, such as tabindex="0", when the scroll container is not the document body, and call refresh() after dynamic content changes.

Navbar scrollspy

Use a nav or navbar whose links point to headings inside the observed scroll container. BCL composes the nav and bs-scrollspy container while Bootstrap owns the active-state updates.

One

Scrollspy watches the scroll container and updates the nav.

Extra content gives the scroll container enough height for deterministic testing.

When this section reaches the top of the scroll area, the first nav item should be active.

Two

Each target heading should have a matching navigation link.

Bootstrap ignores links without matching visible targets, so every link here resolves to an id.

The scrollable region keeps focus support through the tabindex attribute.

Three

Additional paragraphs make the active state change observable as the user scrolls.

This mirrors the official docs pattern of a fixed-height scroll container.

Keep target headings visible and separated enough for the observer to identify them.

Four

The final section confirms the scroll range has a measurable end state.

Smooth scrolling is enabled for navigation clicks inside this example.

aspnet

<bs-row x-gutter="Default3" y-gutter="Default3">
    <bs-div span="Four">
        <bs-nav id="docs-spy-nav" nav-style="Pills" nav-orientation="Vertical">
            <bs-nav-item href="#spy-one">One</bs-nav-item>
            <bs-nav-item href="#spy-two">Two</bs-nav-item>
            <bs-nav-item href="#spy-three">Three</bs-nav-item>
            <bs-nav-item href="#spy-four">Four</bs-nav-item>
        </bs-nav>
    </bs-div>
    <bs-div span="Eight">
        <bs-scrollspy target="#docs-spy-nav" root-margin="0px 0px -25%" threshold="0.1,0.5,1" height="16rem" overflow-y="Scroll" smooth="true" tabindex="0" borders="All" border-rounding="All" padding="Default3">
            <bs-heading heading-level="Three" id="spy-one" heading-style-level="Five">One</bs-heading>
            <bs-p>Scrollspy watches the scroll container and updates the nav.</bs-p>
            <bs-p>Extra content gives the scroll container enough height for deterministic testing.</bs-p>
            <bs-p>When this section reaches the top of the scroll area, the first nav item should be active.</bs-p>
            <bs-heading heading-level="Three" id="spy-two" heading-style-level="Five">Two</bs-heading>
            <bs-p>Each target heading should have a matching navigation link.</bs-p>
            <bs-p>Bootstrap ignores links without matching visible targets, so every link here resolves to an id.</bs-p>
            <bs-p>The scrollable region keeps focus support through the tabindex attribute.</bs-p>
            <bs-heading heading-level="Three" id="spy-three" heading-style-level="Five">Three</bs-heading>
            <bs-p>Additional paragraphs make the active state change observable as the user scrolls.</bs-p>
            <bs-p>This mirrors the official docs pattern of a fixed-height scroll container.</bs-p>
            <bs-p>Keep target headings visible and separated enough for the observer to identify them.</bs-p>
            <bs-heading heading-level="Three" id="spy-four" heading-style-level="Five">Four</bs-heading>
            <bs-p>The final section confirms the scroll range has a measurable end state.</bs-p>
            <bs-p bottom-margin="None">Smooth scrolling is enabled for navigation clicks inside this example.</bs-p>
        </bs-scrollspy>
    </bs-div>
</bs-row>
            

Nested nav

Nested navs can track parent and child sections as long as every link has a visible matching target inside the scroll container.

Item 1

Nested navigation links can track sections and subsections in the same scroll container.

Spacing creates enough scroll range for each child target.

Item 1-1

The first child target has a matching nested navigation item.

Scrollspy applies active state as this subsection reaches the observed area.

Item 1-2

The second child target repeats the same pattern.

Additional content prevents adjacent targets from collapsing into the same view state.

Item 2

The second top-level section verifies transitions between parent items.

Every target remains visible and addressable by id.

Item 2-1

Nested item 2-1 adds another subsection for active-state testing.

Keyboard users can focus the scroll container directly.

Item 2-2

The final nested item gives the scroll range a bottom state.

Scroll to the bottom to activate the final nested link.

aspnet

<bs-row x-gutter="Default3" y-gutter="Default3">
    <bs-div span="Four">
        <bs-nav id="docs-spy-nested-nav" nav-style="Pills" nav-orientation="Vertical">
            <bs-nav-item href="#spy-nested-one">Item 1</bs-nav-item>
            <bs-nav nav-style="Pills" nav-orientation="Vertical" start-margin="Default3" y-margin="Default1">
                <bs-nav-item href="#spy-nested-one-a">Item 1-1</bs-nav-item>
                <bs-nav-item href="#spy-nested-one-b">Item 1-2</bs-nav-item>
            </bs-nav>
            <bs-nav-item href="#spy-nested-two">Item 2</bs-nav-item>
            <bs-nav nav-style="Pills" nav-orientation="Vertical" start-margin="Default3" y-margin="Default1">
                <bs-nav-item href="#spy-nested-two-a">Item 2-1</bs-nav-item>
                <bs-nav-item href="#spy-nested-two-b">Item 2-2</bs-nav-item>
            </bs-nav>
        </bs-nav>
    </bs-div>
    <bs-div span="Eight">
        <bs-scrollspy target="#docs-spy-nested-nav" root-margin="0px 0px -25%" threshold="0.1,0.5,1" height="18rem" overflow-y="Scroll" smooth="true" tabindex="0" borders="All" border-rounding="All" padding="Default3">
            <bs-heading heading-level="Three" id="spy-nested-one" heading-style-level="Five">Item 1</bs-heading>
            <bs-p>Nested navigation links can track sections and subsections in the same scroll container.</bs-p>
            <bs-p>Spacing creates enough scroll range for each child target.</bs-p>
            <bs-heading heading-level="Four" id="spy-nested-one-a" heading-style-level="Six">Item 1-1</bs-heading>
            <bs-p>The first child target has a matching nested navigation item.</bs-p>
            <bs-p>Scrollspy applies active state as this subsection reaches the observed area.</bs-p>
            <bs-heading heading-level="Four" id="spy-nested-one-b" heading-style-level="Six">Item 1-2</bs-heading>
            <bs-p>The second child target repeats the same pattern.</bs-p>
            <bs-p>Additional content prevents adjacent targets from collapsing into the same view state.</bs-p>
            <bs-heading heading-level="Three" id="spy-nested-two" heading-style-level="Five">Item 2</bs-heading>
            <bs-p>The second top-level section verifies transitions between parent items.</bs-p>
            <bs-p>Every target remains visible and addressable by id.</bs-p>
            <bs-heading heading-level="Four" id="spy-nested-two-a" heading-style-level="Six">Item 2-1</bs-heading>
            <bs-p>Nested item 2-1 adds another subsection for active-state testing.</bs-p>
            <bs-p>Keyboard users can focus the scroll container directly.</bs-p>
            <bs-heading heading-level="Four" id="spy-nested-two-b" heading-style-level="Six">Item 2-2</bs-heading>
            <bs-p>The final nested item gives the scroll range a bottom state.</bs-p>
            <bs-p bottom-margin="None">Scroll to the bottom to activate the final nested link.</bs-p>
        </bs-scrollspy>
    </bs-div>
</bs-row>
            

List group

List groups can drive the same active-state behavior as navs. Use link-style list group items and matching target ids.

Item 1

List groups can drive scrollspy active states just like navs.

This content intentionally repeats enough to overflow the container.

Use resolvable anchors for each list group item.

Item 2

The second target has enough spacing to produce an active-state transition.

Short, repeated blocks make this example compact but still testable.

Scroll the panel to watch the list group selection move.

Item 3

Bootstrap updates the matching list group link while this heading is in view.

The scroll container remains independent from the document body.

Keyboard users can focus this scroll region.

Item 4

The final target gives the example a clear bottom state.

The list group should have an active item after scrolling.

aspnet

<bs-row x-gutter="Default3" y-gutter="Default3">
    <bs-div span="Four">
        <bs-listgroup id="docs-spy-list" group-type="Links" flex-direction="Column">
            <bs-listgroup-item href="#spy-list-one">Item 1</bs-listgroup-item>
            <bs-listgroup-item href="#spy-list-two">Item 2</bs-listgroup-item>
            <bs-listgroup-item href="#spy-list-three">Item 3</bs-listgroup-item>
            <bs-listgroup-item href="#spy-list-four">Item 4</bs-listgroup-item>
        </bs-listgroup>
    </bs-div>
    <bs-div span="Eight">
        <bs-scrollspy target="#docs-spy-list" root-margin="0px 0px -25%" threshold="0.1,0.5,1" height="16rem" overflow-y="Scroll" smooth="true" tabindex="0" borders="All" border-rounding="All" padding="Default3">
            <bs-heading heading-level="Three" id="spy-list-one" heading-style-level="Five">Item 1</bs-heading>
            <bs-p>List groups can drive scrollspy active states just like navs.</bs-p>
            <bs-p>This content intentionally repeats enough to overflow the container.</bs-p>
            <bs-p>Use resolvable anchors for each list group item.</bs-p>
            <bs-heading heading-level="Three" id="spy-list-two" heading-style-level="Five">Item 2</bs-heading>
            <bs-p>The second target has enough spacing to produce an active-state transition.</bs-p>
            <bs-p>Short, repeated blocks make this example compact but still testable.</bs-p>
            <bs-p>Scroll the panel to watch the list group selection move.</bs-p>
            <bs-heading heading-level="Three" id="spy-list-three" heading-style-level="Five">Item 3</bs-heading>
            <bs-p>Bootstrap updates the matching list group link while this heading is in view.</bs-p>
            <bs-p>The scroll container remains independent from the document body.</bs-p>
            <bs-p>Keyboard users can focus this scroll region.</bs-p>
            <bs-heading heading-level="Three" id="spy-list-four" heading-style-level="Five">Item 4</bs-heading>
            <bs-p>The final target gives the example a clear bottom state.</bs-p>
            <bs-p bottom-margin="None">The list group should have an active item after scrolling.</bs-p>
        </bs-scrollspy>
    </bs-div>
</bs-row>
            

Simple anchors

Scrollspy can also target a simple anchor collection. This is useful for compact in-page tables of contents that do not need full nav markup.

Alpha

Scrollspy is not limited to nav and list group components.

Plain anchors can also receive the active class when their target is visible.

More content makes the scroll offset measurable.

Beta

The active class moves as this middle target becomes prominent.

Use this pattern for simple in-page table-of-contents links.

Each anchor still needs a matching target id.

Gamma

The last section gives the simple-anchor demo enough range to scroll.

Scroll to the bottom and the final anchor should be active.

aspnet

<bs-div id="docs-spy-simple" display="Flex" flex-wrap="Wrap" gap="Default2" bottom-margin="Default3">
    <a href="#spy-simple-one">Alpha</a>
    <a href="#spy-simple-two">Beta</a>
    <a href="#spy-simple-three">Gamma</a>
</bs-div>
<bs-scrollspy target="#docs-spy-simple" root-margin="0px 0px -25%" threshold="0.1,0.5,1" height="14rem" overflow-y="Scroll" smooth="true" tabindex="0" borders="All" border-rounding="All" padding="Default3" on-activate="console.log('active target', relatedTarget)">
    <bs-heading heading-level="Three" id="spy-simple-one" heading-style-level="Five">Alpha</bs-heading>
    <bs-p>Scrollspy is not limited to nav and list group components.</bs-p>
    <bs-p>Plain anchors can also receive the active class when their target is visible.</bs-p>
    <bs-p>More content makes the scroll offset measurable.</bs-p>
    <bs-heading heading-level="Three" id="spy-simple-two" heading-style-level="Five">Beta</bs-heading>
    <bs-p>The active class moves as this middle target becomes prominent.</bs-p>
    <bs-p>Use this pattern for simple in-page table-of-contents links.</bs-p>
    <bs-p>Each anchor still needs a matching target id.</bs-p>
    <bs-heading heading-level="Three" id="spy-simple-three" heading-style-level="Five">Gamma</bs-heading>
    <bs-p>The last section gives the simple-anchor demo enough range to scroll.</bs-p>
    <bs-p bottom-margin="None">Scroll to the bottom and the final anchor should be active.</bs-p>
</bs-scrollspy>
            

Non-visible elements

Bootstrap ignores targets that are not visible. Keep linked headings in the scroll container layout, and do not point navigation links at collapsed, hidden, or missing targets.

Usage

The BCL helper emits the scroll container attributes. Use target to identify the navigation container, root-margin and threshold to tune Intersection Observer activation, and smooth, height, and overflow utilities to create the documented scroll behavior.

Via data attributes

aspnet

<bs-scrollspy target="#navbar-example" height="200px" smooth-scroll="true" tabindex="0">
<bs-heading heading-level="Four" id="scrollspy-heading-1">First heading</bs-heading>
<bs-p>Observed content.</bs-p>
<bs-heading heading-level="Four" id="scrollspy-heading-2">Second heading</bs-heading>
<bs-p>More observed content.</bs-p>
</bs-scrollspy>
        

Via JavaScript

Use Bootstrap's JavaScript API when content is added, removed, resized, or when the active state must be refreshed after layout changes.

javascript

const scrollSpyElement = document.querySelector('[data-bs-spy="scroll"]');
const scrollSpy = bootstrap.ScrollSpy.getOrCreateInstance(scrollSpyElement);
scrollSpy.refresh();
        

Options

  • Use bootstrap.ScrollSpy.getOrCreateInstance(element) for dynamic scroll containers.
  • Call refresh() after adding, removing, or resizing observed content.
  • root-margin maps to data-bs-root-margin; Bootstrap 5.3 keeps offset only as a deprecated compatibility option.
  • threshold maps to data-bs-threshold and accepts comma-separated Intersection Observer thresholds.
  • Bootstrap raises activate.bs.scrollspy when a new target becomes active.

Methods

Scrollspy methods let application code refresh observed offsets, dispose the instance, or retrieve an existing instance.

javascript

const scrollSpy = bootstrap.ScrollSpy.getOrCreateInstance('[data-bs-spy="scroll"]');
scrollSpy.refresh();
scrollSpy.dispose();
        

Events

Use on-activate to bind the Bootstrap activate.bs.scrollspy event through the helper surface.

aspnet

<bs-scrollspy target="#navbar-example" tabindex="0" on-activate="console.log('active target', relatedTarget)">
...
</bs-scrollspy>