Scrollspy

Highlight the nav link for whichever page section is currently in view.

Basic Usage

Wrap your nav in <c-ui.scrollspy>, mark each section with data-spy-section id="...", then bind link styling to isActive('id'). It tracks the viewport by default; nest the sections in a data-spy-root scroll container for a self-contained area, as below.

Clicking a link calls scrollTo('id') to bring its section into view. To land it just below the top instead of flush against it (handy when a sticky header would otherwise cover it), add a scroll-mt-* utility to the section, e.g. scroll-mt-24. The offset is honoured by both native anchor jumps and scrollTo.

Introduction

Scrollspy keeps a table of contents in sync with the reader's position. As each section below crosses the top of this panel, its link on the left lights up automatically. Scroll slowly to watch the active state hand off from one to the next.

Installation

Wrap your nav and content in the component, then mark every section you want tracked with data-spy-section and a matching id. There is no scroll-position maths to wire up by hand.

Usage

Each section is observed and the topmost one intersecting the panel wins. Bind your link styling to isActive('id') and the highlight follows the scroll, while a click smooth-scrolls that section into view.

Summary

Mark sections, bind links and the scrollspy does the rest. Add a scroll-mt-* utility, as on each section here, when you want a little breathing room above the heading rather than landing it flush.

<c-ui.scrollspy>
    <nav class="flex flex-col gap-2">
        <a href="#intro" :class="isActive('intro') ? 'text-accent font-medium' : 'text-zinc-500'">Introduction</a>
        <a href="#install" :class="isActive('install') ? 'text-accent font-medium' : 'text-zinc-500'">Installation</a>
        <a href="#usage" :class="isActive('usage') ? 'text-accent font-medium' : 'text-zinc-500'">Usage</a>
        <a href="#summary" :class="isActive('summary') ? 'text-accent font-medium' : 'text-zinc-500'">Summary</a>
    </nav>
    {# Omit data-spy-root to track the page viewport instead #}
    <div data-spy-root class="h-72 overflow-y-auto">
        {# scroll-mt-* is optional: it sets the gap above the heading when scrolled to #}
        <section id="intro" data-spy-section class="scroll-mt-4">...</section>
        <section id="install" data-spy-section class="scroll-mt-4">...</section>
        <section id="usage" data-spy-section class="scroll-mt-4">...</section>
        <section id="summary" data-spy-section class="scroll-mt-4">...</section>
    </div>
</c-ui.scrollspy>
{% cotton ui.scrollspy %}
    <nav class="flex flex-col gap-2">
        <a href="#intro" :class="isActive('intro') ? 'text-accent font-medium' : 'text-zinc-500'">Introduction</a>
        <a href="#install" :class="isActive('install') ? 'text-accent font-medium' : 'text-zinc-500'">Installation</a>
        <a href="#usage" :class="isActive('usage') ? 'text-accent font-medium' : 'text-zinc-500'">Usage</a>
        <a href="#summary" :class="isActive('summary') ? 'text-accent font-medium' : 'text-zinc-500'">Summary</a>
    </nav>
    {# Omit data-spy-root to track the page viewport instead #}
    <div data-spy-root class="h-72 overflow-y-auto">
        {# scroll-mt-* is optional: it sets the gap above the heading when scrolled to #}
        <section id="intro" data-spy-section class="scroll-mt-4">...</section>
        <section id="install" data-spy-section class="scroll-mt-4">...</section>
        <section id="usage" data-spy-section class="scroll-mt-4">...</section>
        <section id="summary" data-spy-section class="scroll-mt-4">...</section>
    </div>
{% endcotton %}

API Reference

Scrollspy

Name Description Type Options Default
class Additional classes merged onto the wrapper element. str
slot (default) The nav links. Bind each link's active styling to isActive('id').

Exposes (Alpine)

Name Description Type Options Default
active The id of the section currently in view. str null
isActive(id) Returns true when the given section id is the active one. bool

Sections

Name Description Type Options Default
data-spy-section Mark each watched section with this attribute plus an id matching your links. attr
scroll-mt-* Optional. A scroll-margin-top utility on a section sets how far below the top it lands when scrolled to, so a sticky header won't cover it. Respected by both anchor jumps and scrollTo. class