Skip to content
75NeoUI

component · react

TableOfContents

A rail of heading links that follows the reading position, with an indicator that slides to the section on screen.

tableOfContents

Install

Package manager

Import the theme

The cascade

Tokens

Pass the page’s headings and the rail tracks which of them are on screen, moving its indicator and marking the active link as the reader scrolls.

<TableOfContents
  items={[
    { value: "install", depth: 2, label: "Install" },
    { value: "peer-dependencies", depth: 3, label: "Peer dependencies" },
    { value: "the-cascade", depth: 2, label: "The cascade" },
  ]}
/>
<TableOfContents
  :items="[
    { value: 'install', depth: 2, label: 'Install' },
    { value: 'peer-dependencies', depth: 3, label: 'Peer dependencies' },
    { value: 'the-cascade', depth: 2, label: 'The cascade' },
  ]"
/>

The rail on the right of this page is this component, reading the headings this page generated.

Items

value is the id of the heading element on the page, not a slug of the label. It is resolved with getElementById to know what is on screen, and the link targets it with a hash, so an entry whose id is not on the page renders but never activates. Ids are global to a document, so prefix them when one page holds more than one rail.

depth is the heading level as a number: 2 for an h2, 3 for an h3. Indentation is styled off the data-depth attribute rather than declared as a variant, because one list holds headings at several levels and a variant resolves once for the whole component. Levels past four share the deepest indent rather than marching off the edge.

What it watches

By default the rail watches the page. Pass scrollEl when the prose scrolls inside a panel instead, and rootMargin to move the line at which a heading starts counting as read.

<TableOfContents items={items} scrollEl={() => panelRef.current} />
<TableOfContents :items="items" :scroll-el="() => panel" />

Slots

The same word names the recipe slot, the data-slot attribute and the key in ui.

  • base
  • nav
  • title
  • list
  • indicator
  • item
  • link

Variants

Read off the recipe, so these are the values that actually resolve. A Theme layer can change which one is the default; it cannot add a value.

color
primarysecondarysuccessinfowarningerrorneutral
size
smmdlg

Props

Taken by both adapters, read out of the component module. The same names and the same types work in React and Vue.

ui

TableOfContentsUI

Per-slot class overrides.

color

"primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral"

Defaults to "primary"

size

"sm" | "md" | "lg"

Defaults to "md"

itemsrequired

TableOfContentsItem[]

The headings to list, in document order.

title

string

Heading above the rail.

Defaults to "On this page"

defaultActiveIds

string[]

Which headings count as active up front, before anything has scrolled.

rootMargin

string

The `IntersectionObserver` margin deciding when a heading counts as read.

autoScroll

boolean

Scroll the rail itself so the active entry stays visible.

Defaults to true

React only

Also takes Omit<React.HTMLAttributes<HTMLElement>, "color" | "dir" | "title">, and Pick<React.ComponentProps<typeof Ark.Root>, "activeIds" | "onActiveChange">. A className among them reaches the base slot.

scrollEl

() => HTMLElement | null

The scrolling element to watch. Defaults to the page itself, which is what a documentation page wants; pass one when the prose scrolls inside a panel.