Table of Contents
The “On this page” outline of a document — a list or a rail that opens on hover or focus, a scroll spy, and a sheet for narrow screens.
- Status
- Since
0.23.74- Accessibility pattern
- nav of links, aria-current location, arrow keys
Last updated
Kitchen circuit
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Supply and isolation
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Wiring
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Cable sizes
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Earthing
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Panel labels
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Sign-off
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Install
Add Table of Contents from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/table-of-contentsThe same command installs the registry items it composes: @vegastack/sheet, @vegastack/use-list-nav, @vegastack/use-media-query.
Usage
import { TableOfContents } from "@/components/ui/table-of-contents";
<TableOfContents
variant="rail"
items={outline} // TextEdit's onOutlineChange items, or { id, level, text }[]
onNavigate={(id) => handle.current?.scrollToHeading(id)}
/>;Feed it the headings of the page in document order. TextEdit hands them over through
onOutlineChange, and both TextEdit and MarkdownView (headingIds) put the matching id on
each heading, which is all the scroll spy needs: the marked item is the last heading whose top has
passed a line min(180px, 28%) down the scroller, and the last heading once the scroller reaches
its bottom — so a short closing section still lights up. Choosing an item calls onNavigate;
without it, the heading is scrolled to the top (instantly under reduced motion — give headings a
scroll-margin-top to clear a sticky header).
The outline is sticky by default: set --table-of-contents-top through its className (your
header's height, [--table-of-contents-top:--spacing(14)]) and it sticks there and scrolls within
the rest of the viewport. It renders nothing when no
heading is in range, so an empty page shows no outline.
Wide and narrow are two instances. Show the rail (or list) in a column from a breakpoint,
and the trigger instance — a header button that opens the outline in a left Sheet — below it:
<TableOfContents
items={outline}
onNavigate={go}
trigger={
<Button variant="ghost" size="sm" className="lg:hidden">
Outline
</Button>
}
/>
<TableOfContents variant="rail" items={outline} onNavigate={go} className="hidden lg:block" />Kitchen circuit
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Supply and isolation
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Wiring
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Cable sizes
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Earthing
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Panel labels
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Sign-off
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Scope
- Owns: the outline's links, the scroll spy (
useActiveHeading, exported), the rail's open and close, the sticky placement and the narrow-screen sheet. - Does not own: the headings or their ids (
TextEdit,MarkdownView), or scrolling to a heading when you passonNavigate(TextEdit'sscrollToHeading). - Compose with:
TextEditonOutlineChangeandhandleRef,MarkdownViewheadingIds, and a headerButtonas thetrigger.
Anatomy
Examples
Rail
Short ticks whose width follows the level; the reached one is thicker. Rest the pointer on the rail, or Tab into it, and it opens into the labelled list over the content beside it;
Esc or leaving closes it. A rail row is 28px tall and 24px wide, so each tick is a full-size target.
Kitchen circuit
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Supply and isolation
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Wiring
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Cable sizes
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Earthing
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Panel labels
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Sign-off
Keep the cooker on its own radial circuit, test the RCD before the inspection and note the readings on the schedule.
Controlled active item
Pass activeId to decide the marked item yourself — onActiveIdChange still reports what the spy
has reached. The marked item carries aria-current="location", a leading bar and a heavier weight,
never colour alone.
Levels
Levels 1 to 3 show by default (minLevel, maxLevel); indentation is relative to the shallowest
level shown.
Sheet for narrow screens
With trigger, the outline opens in a left Sheet titled with the label; choosing a heading
closes the sheet, then calls onNavigate, so the page's scroll is unlocked by the time it moves.
Empty
No heading in range renders nothing — there is no empty message to show.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
items* | readonly TableOfContentsItem[] | — | The headings, in document order — TextEdit's onOutlineChange items as they come. |
activeId | string | — | The active heading, controlled. Omit it and the built-in scroll spy decides; null marks
none. |
label | string | "On this page" | The navigation's accessible name and its visible title (the list's heading, the rail's open panel, the sheet's title). |
maxLevel | number | 3 | The deepest heading level shown. |
minLevel | number | 1 | The shallowest heading level shown. |
onActiveIdChange | ((id: string | null) => void) | — | Called with the heading the scroll spy has reached, whenever that changes — pair it with
activeId to control the active item. |
onNavigate | ((id: string) => void) | — | Called when an item is chosen — a click, or Enter/Space on the focused item. Scroll there
yourself (handle.scrollToHeading(id)). When omitted, the heading document.getElementById(id)
is scrolled to the top of its scroller — smoothly, or instantly under reduced motion; give it a
scroll-margin-top to clear a sticky header. Modified clicks (new tab, copy link) are left to
the browser. |
scrollContainer | React.RefObject<HTMLElement | null> | undefined (the nearest scroller) | The element that scrolls the headings, for the scroll spy. Omit it to use the first heading's nearest scrolling ancestor, or the window when the page itself scrolls. |
sticky | boolean | true | Stick to the top of the scroller, at --table-of-contents-top (zero by default; set it through
className, e.g. [--table-of-contents-top:--spacing(14)] for a 56px header), and scroll
within 100dvh minus that offset. |
trigger | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | — | Show the outline in a Sheet (side left) opened by this element instead of inline — for
narrow screens. Choosing an item closes the sheet, then calls onNavigate. variant and
sticky do not apply; ref and className reach the list's nav inside the sheet. |
variant | TableOfContentsVariant | "list" | list is an always-labelled, indented list. rail is a column of short ticks (their width
follows the level, the active one is thicker) that opens into the labelled list on hover or
keyboard focus, over the content beside it. |
TableOfContentsItem
| Prop | Type | Default | Description |
|---|---|---|---|
id* | string | — | The heading element's id — TextEdit (onOutlineChange) and MarkdownView (headingIds) set it. |
level* | number | — | The heading level, 1 for #/h1. Indentation is relative to the shallowest level shown. |
text* | string | — | The heading's text. |
useActiveHeading
useActiveHeading(ids, options) returns the id of the heading the reader has reached, by the same
rule the component uses, for hosts that draw their own outline.
| Prop | Type | Default | Description |
|---|---|---|---|
container | React.RefObject<HTMLElement | null> | undefined (the nearest scroller) | The element that scrolls the headings. Omit it to use the first heading's nearest scrolling
ancestor (an app shell's main, say), or the window when nothing between it and the page
scrolls. |
offset | number | Math.min(180, 28% of the container's visible height) | The activation line, in px below the top edge of the container's visible box. A heading at or above the line has been reached. Set it to your sticky header's height plus a little. |
Accessibility
- A
<nav>landmark named by its visible title (label, "On this page"), holding an ordered list of real links (href="#id"), so copying a link or opening it in a new tab still works; a plain click or Enter is handed toonNavigate. - The reached heading's link carries
aria-current="location"; the visible marker is a bar (list) or a thicker tick (rail) plus weight, never colour alone. - The list is one tab stop, and tabbing in lands on the reached heading. The rail opens on keyboard focus so its labels are visible, and its links keep their names while it is closed.
- In a sheet, focus moves into the dialog, Esc closes it and focus returns to the trigger.
| Key | Action |
|---|---|
| Tab | Into the outline, on the reached heading; out. |
| ↑ / ↓ | Previous / next heading. |
| Home / End | First / last heading. |
| Enter / Space | Go to the heading (onNavigate). |
| Esc | Close the open rail, or the sheet. |
| Contract | States tested |
|---|---|
| Behaviour | default, active, spy-activation-line, spy-bottom-forces-last, rail-collapsed, rail-expanded, sheet-open, empty-renders-nothing, items-changed |
| Accessibility | semantic-html, landmark-region, labeled, aria-current, keyboard-navigation, focus-visible, expanded, focus-return, browser-accessibility-test |
| Visual | default, hover, focus, active, collapsed, expanded |
Do / Don't
Folder Tree
A Notion-style library tree — sections of folders, pages and files, lazy folders, tree keys, row menus, drag into a folder, and a Move picker.
Page Header
The standardized header at the top of a page — back button, breadcrumb trail, title, description, actions, secondary menu, and a favorite star.