The useScrollspy composable provides automatic navigation highlighting based on scroll position. It tracks the visibility of content elements and automatically updates the active state of corresponding navigation items, making it perfect for table of contents, documentation navigation, and section-based layouts.

Basic Usage

The most common use case is to track scroll position within a scrollable container and highlight corresponding navigation items:

Section 1

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Section 2

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Section 3

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

HTML
vue
<template>
  <BContainer>
    <BRow>
      <BCol cols="4">
        <BNav pills vertical ref="navTarget">
          <BNavItem href="#section1" @click="scrollIntoView">Section 1</BNavItem>
          <BNavItem href="#section2" @click="scrollIntoView">Section 2</BNavItem>
          <BNavItem href="#section3" @click="scrollIntoView">Section 3</BNavItem>
        </BNav>
      </BCol>
      <BCol cols="8">
        <div
          ref="scrollContent"
          style="height: 300px; overflow-y: auto; border: 1px solid #dee2e6; padding: 1rem;"
        >
          <h4 id="section1">Section 1</h4>
          <p v-for="i in 4" :key="`s1-${i}`">{{ loremText }}</p>
          <h4 id="section2">Section 2</h4>
          <p v-for="i in 4" :key="`s2-${i}`">{{ loremText }}</p>
          <h4 id="section3">Section 3</h4>
          <p v-for="i in 4" :key="`s3-${i}`">{{ loremText }}</p>
        </div>
      </BCol>
    </BRow>
  </BContainer>
</template>

<script setup lang="ts">
import {useTemplateRef} from 'vue'
import {useScrollspy} from 'bootstrap-vue-next'

const loremText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'

const scrollContent = useTemplateRef('scrollContent')
const navTarget = useTemplateRef('navTarget')

const {scrollIntoView} = useScrollspy(scrollContent, navTarget)
</script>

Manual Mode

When you need more control over the active states, you can use manual mode and work with the list of tracked elements:

Current: None
Introduction

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Features

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Conclusion

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

HTML
vue
<template>
  <BContainer>
    <BRow>
      <BCol cols="4">
        <BListGroup>
          <BListGroupItem
            v-for="item in filteredList"
            :key="item.id as PropertyKey"
            :href="`#${item.id}`"
            :active="current === item.id"
            @click="scrollIntoView"
          >
            {{ item.text }}
          </BListGroupItem>
        </BListGroup>
      </BCol>
      <BCol cols="8">
        <div>
          Current: <strong>{{ current || 'None' }}</strong>
        </div>
        <div
          ref="content"
          style="height: 250px; overflow-y: auto; border: 1px solid #dee2e6; padding: 1rem;"
        >
          <h5 id="intro">Introduction</h5>
          <p v-for="i in 3" :key="`intro-${i}`">{{ loremText }}</p>
          <h5 id="features">Features</h5>
          <p v-for="i in 3" :key="`features-${i}`">{{ loremText }}</p>
          <h5 id="conclusion">Conclusion</h5>
          <p v-for="i in 3" :key="`conclusion-${i}`">{{ loremText }}</p>
        </div>
      </BCol>
    </BRow>
  </BContainer>
</template>

<script setup lang="ts">
import {computed} from 'vue'
import {useTemplateRef} from 'vue'
import {useScrollspy} from 'bootstrap-vue-next'

const loremText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'

const content = useTemplateRef('content')

const {current, list, scrollIntoView} = useScrollspy(content, null, {
  manual: true,
})

const filteredList = computed(() => list.value.filter(item => item.id))
</script>

Custom Content Query

You can customize which elements are tracked using the contentQuery option:

Introduction

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Main Content

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Sidebar Content

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

HTML
vue
<template>
  <BContainer>
    <BRow>
      <BCol cols="4">
        <BNav pills vertical ref="navTarget">
          <BNavItem href="#custom-intro" @click="scrollIntoView">Introduction</BNavItem>
          <BNavItem href="#custom-main" @click="scrollIntoView">Main Content</BNavItem>
          <BNavItem href="#custom-sidebar" @click="scrollIntoView">Sidebar</BNavItem>
        </BNav>
      </BCol>
      <BCol cols="8">
        <div
          ref="content"
          style="height: 300px; overflow-y: auto; border: 1px solid #dee2e6; padding: 1rem;"
        >
          <div class="content-section" id="custom-intro">
            <h4>Introduction</h4>
            <p v-for="i in 3" :key="`intro-${i}`">{{ loremText }}</p>
          </div>
          <div class="content-section" id="custom-main">
            <h4>Main Content</h4>
            <p v-for="i in 3" :key="`main-${i}`">{{ loremText }}</p>
          </div>
          <div class="content-section" id="custom-sidebar">
            <h4>Sidebar Content</h4>
            <p v-for="i in 3" :key="`sidebar-${i}`">{{ loremText }}</p>
          </div>
        </div>
      </BCol>
    </BRow>
  </BContainer>
</template>

<script setup lang="ts">
import {useTemplateRef} from 'vue'
import {useScrollspy} from 'bootstrap-vue-next'

const loremText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'

const content = useTemplateRef('content')
const navTarget = useTemplateRef('navTarget')

const {scrollIntoView} = useScrollspy(content, navTarget, {
  contentQuery: '.content-section[id]',
})
</script>

Configuration Options

The useScrollspy composable accepts several configuration options:

OptionTypeDefaultDescription
contentQuerystring':scope > [id]'CSS selector for elements to track within the content area
targetQuerystring'[href]'CSS selector for navigation links within the target element
manualbooleanfalseWhen true, doesn't automatically apply active classes to navigation
rootstring | ComponentPublicInstance | HTMLElement | nullnullCustom root element for intersection observer
rootMarginstring'0px 0px -25%'Margin around the root for intersection observer
thresholdnumber | number[][0.1, 0.5, 1]Intersection observer thresholds
watchChangesbooleantrueWhether to watch for DOM changes in the content area

Return Values

The composable returns an object with the following properties:

PropertyTypeDescription
currentReadonly<Ref<string | null>>ID of the currently active element
listReadonly<Ref<ScrollspyList>>Array of tracked elements with their visibility status
contentRef<HTMLElement | undefined>Resolved content element reference
targetRef<HTMLElement | undefined>Resolved target element reference
scrollIntoView(event: MouseEvent) => voidHelper function to scroll to clicked navigation item
updateList() => voidManually update the list of tracked elements
cleanup() => voidClean up intersection observers

ScrollspyList Type

Each item in the list array has the ScrollspyListItem structure as defined in the Types documentation.

Advanced Usage

Custom Root and Margins

For more precise control over when elements are considered "active":

HTML
vue
<template>
  <!-- Placeholder template with actual refs for TypeScript compatibility -->
  <div ref="content">
    <div ref="target"></div>
  </div>
</template>

<script setup lang="ts">
import {useTemplateRef} from 'vue'
import {useScrollspy} from 'bootstrap-vue-next'

// Placeholder refs - these would be actual template refs in a real component
const content = useTemplateRef('content')
const target = useTemplateRef('target')

const {current} = useScrollspy(content, target, {
  root: document.querySelector('#custom-viewport') as HTMLElement,
  rootMargin: '0px 0px -50%', // Element must be 50% visible
  threshold: [0.25, 0.5, 0.75], // Multiple thresholds for smooth transitions
})
</script>

Working with Dynamic Content

When content changes dynamically, you can use updateList() to refresh the tracked elements:

HTML
vue
<template>
  <!-- Placeholder template with actual refs for TypeScript compatibility -->
  <div ref="content">
    <div ref="target"></div>
  </div>
</template>

<script setup lang="ts">
import {nextTick, useTemplateRef} from 'vue'
import {useScrollspy} from 'bootstrap-vue-next'

// Placeholder refs - these would be actual template refs in a real component
const content = useTemplateRef('content')
const target = useTemplateRef('target')

const {updateList} = useScrollspy(content, target)

// Call when content changes
const addNewSection = () => {
  // Add new content...
  nextTick(() => {
    updateList()
  })
}
</script>

Cleanup

Always call cleanup() when the component is unmounted to prevent memory leaks:

HTML
vue
<template>
  <!-- Placeholder template with actual refs for TypeScript compatibility -->
  <div ref="content">
    <div ref="target"></div>
  </div>
</template>

<script setup lang="ts">
import {onBeforeUnmount, useTemplateRef} from 'vue'
import {useScrollspy} from 'bootstrap-vue-next'

// Placeholder refs - these would be actual template refs in a real component
const content = useTemplateRef('content')
const target = useTemplateRef('target')

const {cleanup} = useScrollspy(content, target)

onBeforeUnmount(() => {
  cleanup()
})
</script>