# Scrollspy

> Highlight the table of contents as the page scrolls.

## Introduction

Scrollspy watches a set of sections and which one is currently in view. The table of contents on the right of every documentation page is the live demo: the matching link gets `active` and `aria-current="true"`. Clicking a link is a normal hash jump — the offset is `scroll-margin`, the motion is `scroll-behavior: smooth`.

Put `scrollspy` and an `id` on each section. The TOC is a set of destinations within the page, so it lives in a labelled `<nav>`; put `table-of-contents` on its list and point each link at `#that-id`. `AutoInit()` starts every `.scrollspy` except those marked `no-autoinit`. Observation is `IntersectionObserver` — there is no scroll listener.

```
<div class="row">
  <div class="s12 m9">
    <div id="introduction" class="section scrollspy">
      <p>Content</p>
    </div>
    <div id="structure" class="section scrollspy">
      <p>Content</p>
    </div>
  </div>
  <div class="hide-on-small-only m3">
    <nav aria-label="On this page">
      <ul class="section table-of-contents">
        <li><a href="#introduction">Introduction</a></li>
        <li><a href="#structure">Structure</a></li>
      </ul>
    </nav>
  </div>
</div>
```

## Initialization

The IIFE bundle exposes `Expressive.ScrollSpy`. Call `init` yourself when you need options other than the defaults, or let `Expressive.AutoInit()` start every `.scrollspy`.

```
document.addEventListener('DOMContentLoaded', function() {
  const elems = document.querySelectorAll('.scrollspy');
  const instances = Expressive.ScrollSpy.init(elems, {
    // specify options here
  });
});
```

Per-instance options can also be passed through AutoInit:

```
Expressive.AutoInit(document.body, {
  ScrollSpy: { scrollOffset: 200 }
});
```

The documentation TOC is `position: sticky`. Scrollspy only updates which link is active.

## Options

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `scrollOffset` | Number | `200` | Offset in pixels. Written as `--md-comp-scrollspy-offset` (section `scroll-margin`) and used as the observer `rootMargin`. |
| `activeClass` | String | `'active'` | Class applied to the active table-of-contents link. |
| `getActiveElement` | Function | see below | Optional. Returns a CSS selector for the element that should receive `activeClass`, given the section’s id. The default finds `a[href="#id"]` by comparing attributes — it does not interpolate the id into a selector. |
| `keepTopElementActive` | Boolean | `false` | If true, keep the last section above the viewport active when nothing intersects. If there is no such section, the first one stays active. |

### getActiveElement

Only needed when the TOC is not `a[href="#id"]`. Return a CSS selector. Escape any author-controlled id yourself.

```
function(id) {
  return '[data-section="' + CSS.escape(id) + '"]';
}
```

## Methods

> All methods are called on the plugin instance. You can get the instance like this:

```
const instance = Expressive.ScrollSpy.getInstance(elem);
```

### .destroy();

Unobserve the section and tear down the shared observer when it is the last spy.

```
instance.destroy();
```

## Properties

| Name | Type | Description |
| --- | --- | --- |
| `el` | Element | The DOM element the plugin was initialized with (the `.scrollspy` section). |
| `options` | Object | The options the instance was initialized with. |
