0%
/
Table of Contents

Introduction

Good navigation doesn't announce itself. It simply ensures you never feel lost. The Table of Contents is built on this principle — present when needed, invisible when not, always oriented to where you are in the document.

Design Decisions

Most table of contents implementations are either too prominent or too hidden. This one lives in the margin — occupying negative space that would otherwise go to waste. The minimap metaphor comes from code editors, applied to long-form reading.

The Minimap Approach

Each section is represented as a dash, not text. The active dash extends and darkens. This creates a spatial sense of document structure without requiring the reader to process language — just position and weight. Hover triggers a smooth reveal of the full labelled menu.

Mobile Bottom Bar

On narrow viewports, a floating pill anchors to the bottom of the screen — always within thumb reach. It shows the current section title and expands upward into a full list. A scroll-to-top button appears alongside it once you've read past the fold.

Active Section Tracking

Tracking which section is active while scrolling is deceptively complex. An isScrolling ref gates scroll events during programmatic navigation, preventing the active state from flickering through intermediate headings while the page animates.

Scoped vs Window Scroll

When a containerRef is provided, the component binds to that element's scroll event instead of window. Heading positions are measured relative to the container's bounding rect — making it work correctly inside modals, split-pane layouts, or any scrollable div.

Usage

Pass your sections array and matching id attributes on your headings. The component handles everything else. For scoped scroll containers, pass a ref to the scrollable element via containerRef.

Props

sections is required — an array of { id, title, level }. position controls vertical alignment (top | middle | bottom). containerRef scopes to a scrollable element. className allows Tailwind overrides on the outer wrapper.

Accessibility

Every minimap button carries an aria-label with the section title. The mobile trigger is a native button with visible label text. All transitions are under 300ms with ease-out curves, staying within recommended thresholds for UI animations.