Scrollspy
An attachment-based scrollspy component that automatically updates navigation or list group components based on scroll position to indicate which section of the page is currently active in the viewport.
How It Works
Scrollspy is implemented as a Svelte attachment using the {@attach ...} directive. It works by:
- Using the IntersectionObserver API to monitor when elements scroll into view
- Calling a callback function when elements intersect with the scrolling container
- Updating the active state of navigation elements based on the currently visible content
To use Scrollspy, you need:
- A scrollable container element with the
{@attach scrollspy(...)}directive - A set of navigation elements (such as
Nav.Link,ListGroup.Item, or simple anchors) - Since we are using Svelte, it is not necessary to have content elements with IDs that match the
hrefattributes of the navigation elements. However, it is a good practice to ensure that the IDs are unique and correspond to the navigation links. - A callback function to update a Svelte state variable to control the active state of navigation elements.
List Group Example
Scrollspy works seamlessly with Bootstrap list groups. This example shows how to use Scrollspy with a ListGroup component.
Item 1
This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.
Item 2
This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.
Item 3
This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.
Item 4
This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.
<script lang="ts">
import { ListGroup, scrollspy } from '$lib/index.js';
// State to track which list group item should be active
let activeListGroupItemId: string | null = $state(null);
// Callback function for intersection events
const listGroupCallback: IntersectionObserverCallback = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
activeListGroupItemId = entry.target.id;
}
});
};
</script>
<div class="row">
<div class="col-4">
<ListGroup.Root id="list-example">
<ListGroup.Item
href="#list-item-1"
isAction
isActive={activeListGroupItemId === 'list-item-1'}>
Item 1
</ListGroup.Item>
<ListGroup.Item
href="#list-item-2"
isAction
isActive={activeListGroupItemId === 'list-item-2'}>
Item 2
</ListGroup.Item>
</ListGroup.Root>
</div>
<div class="col-8">
<div
{@attach scrollspy({
targetSelector: '#list-example',
callback: listGroupCallback,
observerOptions: { threshold: 0.5 }
})}
class="scrollspy-example"
tabindex="0"
role="presentation">
<h4 id="list-item-1">Item 1</h4>
<p>Content for item 1...</p>
<h4 id="list-item-2">Item 2</h4>
<p>Content for item 2...</p>
</div>
</div>
</div>
Simple Anchors Example
Scrollspy isn't limited to Nav components and list groups - it works with any anchor elements in the document. This example shows how to use Scrollspy with simple anchors.
Item 1
This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.
Item 2
This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.
Item 3
This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.
Item 4
This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.
Item 5
This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.
<script lang="ts">
import { scrollspy } from '$lib/index.js';
// State to track which anchor should be active
let activeSimpleAnchorItemId: string | null = $state(null);
// Callback function for intersection events
const simpleAnchorsCallback: IntersectionObserverCallback = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
activeSimpleAnchorItemId = entry.target.id;
}
});
};
</script>
<div class="row">
<div class="col-4">
<div class="d-flex flex-column gap-2 text-center" id="simple-items">
<a
class="p-1 rounded"
class:active={activeSimpleAnchorItemId === 'simple-item-1'}
href="#simple-item-1">
Item 1
</a>
<a
class="p-1 rounded"
class:active={activeSimpleAnchorItemId === 'simple-item-2'}
href="#simple-item-2">
Item 2
</a>
</div>
</div>
<div class="col-8">
<div
{@attach scrollspy({
targetSelector: '#simple-items',
callback: simpleAnchorsCallback,
observerOptions: { offset: 0, threshold: 0.5 }
})}
class="scrollspy-example"
tabindex="0"
role="presentation">
<h4 id="simple-item-1">Item 1</h4>
<p>Content for item 1...</p>
<h4 id="simple-item-2">Item 2</h4>
<p>Content for item 2...</p>
</div>
</div>
</div>
API Reference
Scrollspy Attachment
The scrollspy function creates an attachment that can be applied to an element with the {@attach ...} directive.
// Type definition for ScrollspyOptions
export type ScrollspyOptions = {
targetSelector: string;
callback: IntersectionObserverCallback;
observerOptions?: IntersectionObserverInit;
};
// Usage example
<div
{@attach scrollspy({
targetSelector: '#element-id', // A CSS selector to the element that contains the anchor targets.
callback: myCallback, // Function to call when elements intersect
observerOptions: { // Standard IntersectionObserver options
rootMargin: '0px 0px -40%', // Margins around the root
threshold: 0.5 // How much of target must be visible to trigger
}
})}
class="scrollspy-container"
tabindex="0">
<!-- Content with elements matching targetSelector -->
</div>
Options
| Option | Type | Default | Description |
|---|---|---|---|
targetSelector | string | Required | CSS selector for elements to observe within the scrolling container |
callback | IntersectionObserverCallback | Required | Function called when elements intersect with the container |
observerOptions | IntersectionObserverInit | { | Configuration options for the IntersectionObserver. Can include rootMargin, threshold, etc. See MDN Documentation |
Usage Notes
- The scrollable container must have
overflow: autooroverflow: scrollCSS applied - For accessibility, give the scrollable container a
tabindex="0"and arole="presentation" - Each target element must have an ID that corresponds to the
hrefattributes in your navigation - The callback should update state variables that control the
isActiveprop on navigation components