Skip to content
HyperUX Experimental
Demo

Track headings inside a scrollable region and keep a table of contents synced to the last visible section. This demo uses nested heading levels, generated active-state styling, and smooth scrolling back into the content panel.

Demo Config

huxScrollSpy()

Table of Contents

Getting Started

Scroll Spy watches headings inside a scrollable container and mirrors that structure into a reactive table of contents. It is useful for docs layouts, long settings panels, and any interface where the content region scrolls independently from the page.

Authoring Markup

The component queries the heading elements you allow through headingSelector and records their level from the tag name. Existing heading ids are preserved, while missing ids are generated automatically from the heading text.

Generated IDs

Generated ids are slug-based and de-duplicated against the current document. That means repeated heading text still produces stable scroll targets without additional authoring work.

If you need specific deep links, add ids in your markup explicitly and the component will keep them as-is.

Active State Behavior

Visibility is driven by IntersectionObserver with the scroll container set as the observer root. When multiple headings are visible, the component chooses the last visible heading in document order.

Bottom of Container

A small scroll-position check keeps the last heading active when the panel is scrolled to the end. This prevents the state from getting stuck on an earlier heading near the bottom edge of the container.

Scroll Navigation

Selecting a TOC item calls scrollToHeadingItem(), which uses smooth scrolling so the transition remains readable.

Configuration Surface

You can point the component at a different content ref, narrow the heading selector, or tune observer sensitivity with custom rootMargin and visibilityThreshold values.


Alpine.js Scroll Spy

The huxScrollSpy utility watches headings inside a scrollable container and exposes a reactive table of contents model for Alpine.js. It is for documentation sidebars, settings panels, and other layouts where content scrolls inside its own region instead of the page.

huxScrollSpy requires a valid x-ref target for the scroll container and a browser environment with IntersectionObserver. The component only manages heading discovery, active-state updates, and scroll-to-section behavior; you control the TOC markup and styling.

API

huxScrollSpy(config)

Returns an Alpine data object with:

Internal helper methods are private implementation details and are not part of the supported API contract.

Options

Quick Start

Minimal

<div x-data="huxScrollSpy()" class="grid gap-4 md:grid-cols-3">
  <nav aria-label="Table of contents" class="space-y-2">
    <template x-for="headingItem in headingItems" x-bind:key="headingItem.id">
      <button
        type="button"
        class="block w-full rounded px-4 py-2 text-left"
        x-bind:class="{
          'bg-blue-100 text-blue-900': activeHeadingId === headingItem.id,
          'hover:bg-gray-100': activeHeadingId !== headingItem.id
        }"
        x-on:click="scrollToHeadingItem(headingItem.id)"
        x-text="headingItem.text"
      ></button>
    </template>
  </nav>

  <div x-ref="tocContent" class="max-h-96 overflow-y-auto md:col-span-2">
    <h2 id="overview">Overview</h2>
    <p>...</p>
    <h3 id="details">Details</h3>
    <p>...</p>
  </div>
</div>

Custom Observer Settings

huxScrollSpy({
  rootMargin: '0px 0px -50% 0px',
  visibilityThreshold: [0, 0.25, 0.5],
})

Common Usage Patterns

Watch a Named Content Ref

<div x-data="huxScrollSpy({ templateRef: 'articleContent' })">
  <nav aria-label="Article sections">
    <template x-for="headingItem in headingItems" x-bind:key="headingItem.id">
      <button
        type="button"
        x-on:click="scrollToHeadingItem(headingItem.id)"
        x-text="headingItem.text"
      ></button>
    </template>
  </nav>

  <article x-ref="articleContent" class="max-h-96 overflow-y-auto">
    <h2>Introduction</h2>
    <h3>Requirements</h3>
    <h2>Implementation</h2>
  </article>
</div>

Restrict Tracked Headings

huxScrollSpy({
  headingSelector: 'h2, h3',
})

Behavior Contract

Error Handling

Accessibility Notes

Notes