---
id: "packages/active-ui/table-of-contents"
title: "TableOfContents"
description: "The contents of a page as links to its headings, with the section in view marked; as a list beside the content or behind a button above it."
url: "/packages/active-ui/table-of-contents"
locale: "en"
revision: "ae4ce03a1102cffc"
---

# TableOfContents

`TableOfContents` lists the headings of a page as links to their anchors and marks the section the
reader is in. Beside the content it is a list; on a narrow screen or at a high zoom it is the same
list behind a button, above the content.

**Example: The contents of a page, in both forms**

Client: Runs entirely in your browser. Source: `examples/ui/table-of-contents.tsx`.

```tsx
// apps/docs/examples/ui/table-of-contents.tsx
'use client';

import {TableOfContents} from '@workspace/active-ui/components/composed/table-of-contents';

const ITEMS = [
  {id: 'toc-example-keys', text: 'Keys', depth: 2},
  {id: 'toc-example-plurals', text: 'Plurals', depth: 2},
  {id: 'toc-example-zero', text: 'Zero and one', depth: 3},
  {id: 'toc-example-missing', text: 'Missing keys', depth: 2},
];

/**
 * The contents of a page in its two forms: the list beside the content, and
 * the collapsible form above it on a narrow screen. The section in view is
 * marked as the reader scrolls past the headings of this example.
 */
export default function TableOfContentsExample() {
  return (
    <div className="grid w-full gap-8 sm:grid-cols-[1fr_14rem]">
      <div className="flex flex-col gap-6 text-sm">
        {ITEMS.map((item) => (
          <section key={item.id} className="flex flex-col gap-1">
            {item.depth === 2 ? (
              <h3 id={item.id} className="font-medium">
                {item.text}
              </h3>
            ) : (
              <h4 id={item.id} className="font-medium">
                {item.text}
              </h4>
            )}
            <p className="text-muted-foreground">
              A section of the page. Scroll past it, and the list marks the
              section you are in.
            </p>
          </section>
        ))}
        <TableOfContents
          items={ITEMS}
          label="On this page"
          variant="collapsible"
          className="border-border border-y"
        />
      </div>
      <TableOfContents items={ITEMS} label="Contents" />
    </div>
  );
}
```

## What the composition adds

- **The section in view**, marked with `aria-current="location"`: the last heading above `offset`
  pixels from the top of the window (the height of a sticky header), or the first heading before
  the reader reaches any. A heading that is not in the document is skipped.
- **Any scroll area**: it follows the scrolling of the window and of any element the page scrolls
  in, such as the content area of an application shell below a fixed header. There is nothing to
  configure.
- **The mark stays in sight**: when you give the list a maximum height so it scrolls on its own (a
  sticky column in a low window), the marked entry is kept in view inside the list; the page itself
  does not move.
- **A mark without a heavier font**: colour and a short bar beside the marked entry only, so the
  list never shifts. There is no line along the list.
- **Two forms**: `variant="list"` (the default) and `variant="collapsible"`, closed at first, built on
  [Collapsible](/packages/active-ui/collapsible).
- **Depth**: the top level of the items is flush, deeper levels are indented.

## Usage

```tsx
import {TableOfContents} from '@workspace/active-ui/components/composed/table-of-contents';

<TableOfContents
  items={[
    {id: 'keys', text: 'Keys', depth: 2},
    {id: 'plurals', text: 'Plurals', depth: 2},
  ]}
  label="On this page"
/>;
```

## What your application provides

- The headings, with the anchors the page renders: for documentation, the table of contents of the
  page (`PageView.toc` of active-docs).
- The label, translated: it names the navigation and is the button of the collapsible form.
- Where each form goes: the list in a column beside the content, the collapsible form above it.

## Accessibility and keyboard

A `<nav>` named by `label`. Every entry is a link, reached with `Tab`; the collapsible form opens
with `Enter` or `Space` on its button.

## API

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | `readonly TableOfContentsItem[]` | yes |  |
| `label` | `string` | yes | The name of the list, also shown: for example "On this page". |
| `offset` | `number` | no | Pixels from the top of the viewport that count as "read". |
| `variant` | `"collapsible" \| "list"` | no |  |

**TableOfContentsItem**

```ts
interface TableOfContentsItem {
  id: string;
  text: string;
  depth: number;
}
```

"On this page": the headings of a page as links to their anchors, with the
section the reader is in marked (`aria-current="location"`). The mark is a
colour and a border, never a heavier font, so the list does not shift.

- `variant="list"` (the default): a labelled list, for a column beside the
content.
- `variant="collapsible"`: the same list behind a button with the label,
closed at first, for a narrow screen or zoom, above the content.

The section in view is the last heading above `offset` pixels from the top
of the viewport (a sticky header's height), or the first heading before
the reader reaches any. Headings that are not in the document are skipped.
It follows every scroll: of the window, and of any element the page
scrolls in (an application shell whose content area scrolls below a fixed
header), with no option to set.

In a list taller than its room, the caller makes the list (or the whole
navigation) scroll, for example with a maximum height; the marked entry
then stays in view inside that scroll area, and nothing outside it moves.
The label is the caller's: the component translates nothing.

**TableOfContentsProps**

```ts
interface TableOfContentsProps extends Omit<
  React.ComponentProps<'nav'>,
  'children'
> {
  items: readonly TableOfContentsItem[];
  label: string;
  variant?: 'list' | 'collapsible';
  offset?: number;
}
```
