Skip to content

Active Heading

hookscrolltocheading

Configurable active heading detection for table of contents. Tracks which heading is currently active based on scroll position, with top-line or viewport-center activation modes and a programmatic scrollTo helper.

tsx
const { activeId, scrollTo } = useActiveHeading({  ids: ["intro", "features", "api"],  activation: "top-line",  topOffset: 96,});

Installation

$pnpm exec dgadd add ui/active-heading
[Installs to]src/hooks/use-active-heading.ts[Item]ui/active-heading

dgadd is not public on npm yet. Until the first release, pack @diffgazer/add from the repository and install that tarball into this app, which is what puts dgadd on pnpm exec.

UI components require Tailwind CSS v4. Local copy mode imports src/styles/styles.css; package mode uses @diffgazer/ui CSS once packages are available.

Parameters

NameTypeDefaultDescription
idsrequiredstring[]Ordered list of heading element IDs to observe. Elements are resolved via document.getElementById.
containerIdstringID of a scrollable container element. When omitted, the window is used as the scroll target.
activation"top-line" | "viewport-center" | number"top-line"How headings activate. "top-line" activates when crossing topOffset from container top. "viewport-center" activates at the vertical center. A number (0–1) sets a custom viewport fraction.
topOffsetnumber96Pixel offset from the container top used by the "top-line" activation mode.
scrollOffsetnumbertopOffsetPixel offset applied when scrollTo programmatically scrolls to a heading. Defaults to topOffset.
bottomMarginnumber0Fraction of viewport height used as bottom margin for bottomLock detection.
thresholdnumber0Fraction (0–1) of the heading element's height that must cross the activation line before it becomes active. 0 = top edge, 1 = bottom edge.
bottomLockbooleantrueWhen true, the last heading is always activated when the user scrolls to the bottom of the container. Ignored while the container has nothing to scroll.
enabledbooleantrueSet to false to disable scroll observation. When disabled, activeId is set to null.
settleDelaynumber150Milliseconds to wait after a programmatic scrollTo before resuming scroll tracking. Prevents flickering during smooth scroll animation.
observebooleantrueWatch for DOM changes via MutationObserver. Disable for static content.

Returns

{ activeId: string | null; scrollTo: (id: string) => void }Object with the currently active heading ID and a function to programmatically scroll to a heading.
NameTypeDefaultDescription
activeIdrequiredstring | nullID of the currently active heading, or null when disabled or no headings are found.
scrollTorequired(id: string) => voidScrolls to the heading with the given ID. Immediately sets activeId to prevent flickering during the scroll animation.

Examples

Basic Table of Contents

Preview

Activation Modes

Preview

Notes

Disabled state

When enabled is false, activeId is null from the first render, including server rendering. Enabling observation later starts from the first available heading; disabling it again clears activeId.

Activation Modes

Use "top-line" (default) for fixed-header layouts where headings activate near the top. Use "viewport-center" for centered reading experiences. Pass a number (0–1) for a custom viewport fraction.

Scroll Settling

When scrollTo is called, scroll-listener updates are suppressed until the animation settles (150ms after the last scroll event). This prevents the active heading from flickering through intermediate headings during programmatic scrolls.

Bottom Lock

When bottomLock is true (default), the last heading is always highlighted when scrolled to the bottom — without it, a last section shorter than the viewport never becomes active. It never fires on a container that cannot scroll, so a static pane keeps its first heading active.

Source

Highlighted source loads after this disclosure opens. Browse the source repository.