# 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.

Source: https://bootstrap-svelte.vercel.app/components/scrollspy — part of the Bootstrap Svelte documentation (index: https://bootstrap-svelte.vercel.app/llms.txt).

## 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 `href` attributes 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.

## Navbar Example

Use Scrollspy with a Bootstrap navbar to highlight the active section as you scroll down the page.

Navbar

- [First](https://bootstrap-svelte.vercel.app/components/scrollspy#scrollspyHeading1)
- [Second](https://bootstrap-svelte.vercel.app/components/scrollspy#scrollspyHeading2)
- [Third](https://bootstrap-svelte.vercel.app/components/scrollspy#scrollspyHeading3)
- [Fourth](https://bootstrap-svelte.vercel.app/components/scrollspy#scrollspyHeading4)
- [Fifth](https://bootstrap-svelte.vercel.app/components/scrollspy#scrollspyHeading5)

```html
<script lang="ts">
import { Nav, Navbar, scrollspy } from '$lib/index.js';

// State to track which link should be active
let activeNavLinkId: string | null = $state(null);

// Callback function that will be executed when elements intersect
const callback: IntersectionObserverCallback = (entries: IntersectionObserverEntry[]) => {
    entries.forEach((entry) => {
        if (entry.isIntersecting) {
            activeNavLinkId = entry.target.id;
        }
    });
};
</script>

<Navbar.Root expandOnBreakpoint={false} id="navbar-example" class="bg-body-tertiary px-3 mb-3">
    <Navbar.Brand href="#!">Navbar</Navbar.Brand>
    <Nav.Root itemStyle="pills">
        <Nav.Item>
            <Nav.Link href="#scrollspyHeading1" isActive={activeNavLinkId === 'scrollspyHeading1'}>First</Nav.Link>
        </Nav.Item>
        <Nav.Item>
            <Nav.Link href="#scrollspyHeading2" isActive={activeNavLinkId === 'scrollspyHeading2'}>Second</Nav.Link>
        </Nav.Item>
    </Nav.Root>
</Navbar.Root>

<!-- Element to be scrolled with scrollspy attached -->
<div
    {@attach scrollspy({
        targetSelector: '#navbar-example',
        callback: callback,
        observerOptions: {
            rootMargin: '0px 0px -40%',
            threshold: 0.5
        }
    })}
    class="scrollspy-example bg-body-tertiary p-3 rounded-2"
    tabindex="0"
    role="presentation">
    <h4 id="scrollspyHeading1">First heading</h4>
    <p>Content for first heading...</p>
    <h4 id="scrollspyHeading2">Second heading</h4>
    <p>Content for second heading...</p>
</div>
```

## Nested Nav Example

Scrollspy also works with nested `Nav` components. When a nested nav item is active, its parent will also be active.

```html
<script lang="ts">
import { Nav, scrollspy } from '$lib/index.js';

// State to track which link should be active
let activeNestedNavId: string | null = $state(null);

// Callback function for intersection events
const nestedNavCallback: IntersectionObserverCallback = (entries: IntersectionObserverEntry[]) => {
    entries.forEach((entry) => {
        if (entry.isIntersecting) {
            activeNestedNavId = entry.target.id;
        }
    });
};
</script>

<div class="row">
    <div class="col-4">
        <Nav.Root id="navbar-example3" verticalBreakpoint="xs" itemStyle="pills">
            <Nav.Link href="#item-1" isActive={activeNestedNavId?.includes('item-1')}>Item 1</Nav.Link>
            <Nav.Root class="ms-3" verticalBreakpoint="xs">
                <Nav.Link href="#item-1-1" isActive={activeNestedNavId === 'item-1-1'}>Item 1-1</Nav.Link>
                <Nav.Link href="#item-1-2" isActive={activeNestedNavId === 'item-1-2'}>Item 1-2</Nav.Link>
            </Nav.Root>
            <Nav.Link href="#item-2" isActive={activeNestedNavId === 'item-2'}>Item 2</Nav.Link>
            <Nav.Link href="#item-3" isActive={activeNestedNavId?.includes('item-3')}>Item 3</Nav.Link>
            <Nav.Root class="ms-3" verticalBreakpoint="xs">
                <Nav.Link href="#item-3-1" isActive={activeNestedNavId === 'item-3-1'}>Item 3-1</Nav.Link>
                <Nav.Link href="#item-3-2" isActive={activeNestedNavId === 'item-3-2'}>Item 3-2</Nav.Link>
            </Nav.Root>
        </Nav.Root>
    </div>
    <div class="col-8">
        <div
            {@attach scrollspy({
                targetSelector: '#navbar-example3',
                callback: nestedNavCallback,
                observerOptions: { threshold: 0.5 }
            })}
            class="scrollspy-example-2"
            tabindex="0"
            role="presentation">
            <!-- Content with IDs matching the Nav.Link href values -->
            <div id="item-1"><h4>Item 1</h4><p>Content for item 1...</p></div>
            <div id="item-1-1"><h5>Item 1-1</h5><p>Content for item 1-1...</p></div>
        </div>
    </div>
</div>
```

## List Group Example

Scrollspy works seamlessly with Bootstrap list groups. This example shows how to use Scrollspy with a `ListGroup` component.

```html
<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.

```html
<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.

```html
// 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` | `{ root:[scrollable container element],rootMargin:'0px 0px -25%',threshold:[0.1, 0.5, 1]}` | Configuration options for the IntersectionObserver. Can include `rootMargin`, `threshold`, etc. See [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver) |

#### Usage Notes

- The scrollable container must have `overflow: auto` or `overflow: scroll` CSS applied
- For accessibility, give the scrollable container a `tabindex="0"` and a `role="presentation"`
- Each target element must have an ID that corresponds to the `href` attributes in your navigation
- The callback should update state variables that control the `isActive` prop on navigation components
