TableOfContents

Indexes a long page and marks the section currently in view.

import { TableOfContents } from "@usefragments/ui";Source

Preview

<TableOfContents>
  <TableOfContents.Item targetId="overview">Overview</TableOfContents.Item>
  <TableOfContents.Item targetId="installation" active>Installation</TableOfContents.Item>
  <TableOfContents.Group label="Usage" collapsible={false}>
    <TableOfContents.Item targetId="controlled">Controlled</TableOfContents.Item>
    <TableOfContents.Item targetId="with-a-form">With a form</TableOfContents.Item>

Usage

Use when

  • Docs, articles, and other long reads
  • Reference pages with many sections
  • Filterable sidebars grouped by category
  • Anything past three headings

Avoid when

  • Pages with one or two sections
  • Site navigation (use Sidebar or Header)
  • Steps in an order (use Stepper)

Implementation notes

  • Drive the active item from an IntersectionObserver scroll spy
  • Group nests items one level deeper under a labelled header that folds at once
  • Pass collapsible={false} for a header that is only a label
  • Item targetId must match the real heading id
  • Items smooth-scroll — call preventDefault() in onClick to take over
  • Group is uncontrolled and open by default; pass open and onOpenChange to drive it
  • Pass title={null} to show no title

Installation

pnpm add @usefragments/ui

The npm package stays managed by your package manager. Import styles once from @usefragments/ui/styles. See the installation guide for framework and theme setup.

Anatomy

Import TableOfContents once, then compose its parts with dot notation.

  • TableOfContents.Item
  • TableOfContents.Group

A docs page's index: the section in view is ink 1, marked on the track.

<TableOfContents>
  <TableOfContents.Item targetId="overview">Overview</TableOfContents.Item>
  <TableOfContents.Item targetId="installation" active>Installation</TableOfContents.Item>
  <TableOfContents.Group label="Usage" collapsible={false}>
    <TableOfContents.Item targetId="controlled">Controlled</TableOfContents.Item>
    <TableOfContents.Item targetId="with-a-form">With a form</TableOfContents.Item>
  </TableOfContents.Group>
  <TableOfContents.Item targetId="api-reference">API reference</TableOfContents.Item>
  <TableOfContents.Item targetId="accessibility">Accessibility</TableOfContents.Item>
</TableOfContents>

Examples

Nested Groups

Groups fold at once; one starts closed.

<TableOfContents title="Components">
  <TableOfContents.Item targetId="all" active>All</TableOfContents.Item>
  <TableOfContents.Group label="Primitives" trailing={<span>3</span>}>
    <TableOfContents.Item targetId="button">Button</TableOfContents.Item>
    <TableOfContents.Item targetId="card">Card</TableOfContents.Item>
    <TableOfContents.Item targetId="input">Input</TableOfContents.Item>

With Active Item

Deep in a nested section: the marker stays on the one track.

<TableOfContents>
  <TableOfContents.Item targetId="overview">Overview</TableOfContents.Item>
  <TableOfContents.Group label="Guides" collapsible={false}>
    <TableOfContents.Item targetId="theming">Theming</TableOfContents.Item>
    <TableOfContents.Group label="Forms" collapsible={false}>
      <TableOfContents.Item targetId="validation" active>Validation</TableOfContents.Item>

Custom Title

Renames the title above the list.

<TableOfContents title="Contents">
  <TableOfContents.Item targetId="chapter-1">Chapter 1: The beginning</TableOfContents.Item>
  <TableOfContents.Item targetId="chapter-2" active>Chapter 2: The middle</TableOfContents.Item>
  <TableOfContents.Item targetId="chapter-3">Chapter 3: The end</TableOfContents.Item>
</TableOfContents>

No Title

title={null} shows the list alone.

<TableOfContents title={null}>
  <TableOfContents.Item targetId="section-a">Section A</TableOfContents.Item>
  <TableOfContents.Item targetId="section-b" active>Section B</TableOfContents.Item>
  <TableOfContents.Item targetId="section-c">Section C</TableOfContents.Item>
</TableOfContents>

API reference

Component props
Prop
Type
Default
Description
childrenRequiredReact.ReactNodeNot setTableOfContents.Item or TableOfContents.Group elements; none renders nothing
Group.childrenRequiredReact.ReactNodeNot set—
Group.collapsibleboolean | undefinedNot setWhether the header opens and closes the group; `false` makes it a plain label over items that always show.
Group.defaultOpenboolean | undefinedNot setWhether the group starts open (uncontrolled).
Group.labelRequiredReact.ReactNodeNot setThe group header's label.
Group.leadingReact.ReactNodeNot setA leading element on the header row (icon).
Group.onOpenChange((open: boolean) => void) | undefinedNot setCalled when the header opens or closes the group.
Group.openboolean | undefinedNot setWhether the group is open (controlled).
Group.trailingReact.ReactNodeNot setA trailing element on the header row (count).
Item.activeboolean | undefinedNot setThe section in view: ink 1 and the marker on the track, and `aria-current="location"`.
Item.childrenRequiredReact.ReactNodeNot set—
Item.leadingReact.ReactNodeNot setA leading element (icon, dot), before the label.
Item.targetIdstring | undefinedNot setThe id of the heading this item scrolls to. The link points at `#targetId` unless `href` is given.
Item.trailingReact.ReactNodeNot setA trailing element (count), after the label.
labelstring | undefinedTable of contentsNames the navigation landmark
titleReact.ReactNodeOn this pageThe title above the list; null shows none

Accessibility

  • Uses <nav aria-label="Table of contents"> for landmark navigation
  • The section in view is marked with aria-current="location"
  • Items are links; in-page links smooth-scroll unless reduced motion is requested
  • A folding group header is a real <button> with aria-expanded; its items sit in a nested list
  • Focus-visible ring on keyboard navigation
  • BreadcrumbscomplementaryBreadcrumbs show hierarchy, TOC shows page sections
  • SidebarcomplementarySidebar for site nav, TOC for in-page nav
  • TabsalternativeTabs for switching views, TOC for scrolling to sections

Next steps