Skip to content

ExpressiveCSS

Browse documentation

Scrollspy

Highlight the table of contents as the page scrolls.

View as Markdown

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

  1. Put scrollspy and an id on each section.
  2. Wrap the TOC in a labelled <nav>, because it is a set of destinations within the page.
  3. Put table-of-contents on its list and point each link at #that-id.

AutoInit() starts every .scrollspy except those marked no-autoinit. Scrollspy uses IntersectionObserver and has no scroll listener.

Each link is at least 48dp tall, the Material touch target. For a denser list, set --md-comp-toc-item-height on the .table-of-contents list.

<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-compact-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
  });
});

You can also pass per-instance options 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. Scrollspy writes it to --md-comp-scrollspy-offset (section scroll-margin) and uses it 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 and 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

Return a CSS selector. Escape any author-controlled id yourself. Sections sharing the same callback function share one current link. For independent tables of contents, use a separate callback for each navigation and reuse it for all its sections. The default callback groups all default instances together. Each section honors its own scrollOffset and activeClass.

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();

Disconnect the section's observer, restore its original offset style, and update the current link within its group. Calling it more than once is safe.

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.

No matching pages

  • Source color

    The seed every generated ramp derives from. Pick one and browse the docs — the whole theme follows. Error does not: it is a fixed hue.

    Default source: Mist Iris.

    ExpressiveCSS presets: Lagoon, Harbor, Breeze, Cobalt, Iris.

    Bold

    Soft

    Mist

    Glow