Table of Contents

An anchor list that tracks which heading is on screen, through IntersectionObserver rather than scroll offsets. Reads the DOM by default, so it works for rendered markdown.

src/components/ui/table-of-contents.tsx

About

From the component's own source, where it was written.

An anchor list that tracks which heading is on screen.

Scroll-spy through IntersectionObserver, not scroll offsets. Measuring positions on every scroll event means reading layout in a handler that fires dozens of times a second, and the browser already computes this.

The rootMargin pulls the detection band up to the top quarter of the viewport. Without it the active item is whatever is nearest the bottom, so the highlight sits one or two headings ahead of what you are reading.

Headings are found in the DOM rather than passed in, so it stays correct for content rendered from markdown where the caller has no list to give.

Install

npx astralyx-ui add table-of-contents

Usage

import { TableOfContents } from '@/components/ui/table-of-contents'

<TableOfContents selector="h2, h3" container="#doc" />

Composer

Change the props and the source below follows.

Props
tsx
<TableOfContents selector="h2, h3" label="On this page" />

Examples

Anchors

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

TableOfContents

Extends ComponentProps<'nav'>.

PropTypeDefaultDescription
itemsTocItem[]—Supply headings directly, or omit and let it read the DOM.
selectorstring'h2, h3'What to collect and where. Headings need ids.
containerstring'h2, h3'What to collect and where. Headings need ids.
labelstring'On this page'—

Hooks

HookSignatureDescription
useHeadings(selector: string, container?: string) => TocItem[]Exported, for building your own list.

Types

TocItem

An anchor list that tracks which heading is on screen.

FieldTypeDescription
idstring—
labelstring—
levelnumber—

Behaviour

TopicDetailDescription
scroll-spyIntersectionObserverThe rootMargin pulls the detection band to the top quarter of the viewport — without it the highlight sits one or two headings ahead of what you are reading.