<!-- Protocore Design System — TableOfContents -->
# TableOfContents

- **Category:** Navigation (`navigation`)
- **Slug:** `navigation/table-of-contents`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { TableOfContents } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/navigation/table-of-contents

> A page nav auto-built from headings, with scrollspy active-highlighting — the reusable docs rail.

## Scanning the DOM

Omit `headings` and the component scans the page for you: give it a `containerSelector` (default `"main"`) and a `selector` (default `"h2, h3"`), and it collects every matching heading that has an `id`. This is the mode to pair with **Prose** — render your CMS/markdown content (whose headings already carry slugged ids), drop a `TableOfContents` beside it, and the rail builds itself.

The anchors are ordinary links, so the rail works with JavaScript disabled; the `IntersectionObserver` only drives the active-highlight. Clicking smooth-scrolls to the target and moves keyboard focus there. Tune `offset` to match any sticky header so the active section flips at the right moment.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `className` | `string` | no | — | — |
| `containerSelector` | `string` | no | `main` | CSS selector for the region to scan. |
| `headings` | `TocHeading[]` | no | — | Explicit headings. Omit to scan the DOM via `containerSelector` + `selector`. |
| `label` | `string` | no | `Table of contents` | Accessible name for the nav landmark. |
| `offset` | `number` | no | `96` | Distance (px) from the top at which a heading becomes active. |
| `selector` | `string` | no | `h2, h3` | Which headings to collect when scanning. |
| `style` | `CSSProperties` | no | — | — |

## Examples

### Explicit headings

Pass a `headings` array of `{ id, text, level }`. Each renders as a real anchor to `#id`, indented by level. As the reader scrolls, the entry for the section in view highlights via scrollspy.

```tsx
import { TableOfContents, Heading, Text } from "@protocore/pds";

const headings = [
  { id: "toc-overview", text: "Overview", level: 2 },
  { id: "toc-install", text: "Installation", level: 2 },
  { id: "toc-config", text: "Configuration", level: 3 },
  { id: "toc-deploy", text: "Deploying", level: 2 },
];

export default function TableOfContentsBasics() {
  return (
    <div style={{ display: "grid", gridTemplateColumns: "160px 1fr", gap: 32 }}>
      <TableOfContents
        headings={headings}
        style={{ position: "sticky", top: 0, alignSelf: "start" }}
      />
      <main style={{ maxWidth: "48ch" }}>
        {headings.map((h) => (
          <section key={h.id} style={{ marginBottom: 24 }}>
            <Heading as="h2" size="title" id={h.id}>
              {h.text}
            </Heading>
            <Text size="sm" color="muted" style={{ marginTop: 8 }}>
              Section content for {h.text.toLowerCase()}. Scroll the page to watch the rail track the
              heading in view.
            </Text>
          </section>
        ))}
      </main>
    </div>
  );
}
```

## Usage

**Do**

- Ensure every target heading has a stable `id` to anchor to.
- Use scan mode next to Prose-rendered content.
- Set `offset` to your sticky-header height for accurate spy timing.
- Keep it to two levels (h2/h3) so the rail stays scannable.

**Don't**

- Point entries at headings without ids; the anchor has nowhere to go.
- Nest four-plus levels; the rail becomes a thicket.
- Rebuild it from scroll math — it already uses an observer.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Tab` | Moves through the table-of-contents links |
| `Enter` | Jumps to the section and moves focus there |

**Notes**

- Renders a `<nav>` landmark with an `aria-label`, and marks the active entry with `aria-current="location"`.
- Activating a link moves keyboard focus to the target heading (via a temporary `tabindex`), so the next Tab continues from the new section.

## Related

`nav-link`, `breadcrumb`, `prose`, `sidebar`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
