# Table of contents

Links to the sections of the current page, usually in a side region of a long page.

```tsx
// demo.tsx
import {TableOfContents} from '@earendil-works/elanor-react';

export default function TableOfContentsDemo() {
  return (
    <TableOfContents.Root>
      <TableOfContents.List>
        <TableOfContents.Item>
          <TableOfContents.Link href="#usage">Usage</TableOfContents.Link>
        </TableOfContents.Item>
        <TableOfContents.Item>
          <TableOfContents.Link href="#current-section">Current section</TableOfContents.Link>
        </TableOfContents.Item>
        <TableOfContents.Item>
          <TableOfContents.Link href="#api">API</TableOfContents.Link>
        </TableOfContents.Item>
      </TableOfContents.List>
      <TableOfContents.Indicator />
    </TableOfContents.Root>
  );
}
```

## Usage

```tsx
import {TableOfContents} from '@earendil-works/elanor-react';

<TableOfContents.Root>
  <TableOfContents.List>
    <TableOfContents.Item>
      <TableOfContents.Link href="#usage" active>
        Usage
      </TableOfContents.Link>
    </TableOfContents.Item>
  </TableOfContents.List>
  <TableOfContents.Indicator />
</TableOfContents.Root>;
```

The demo links point to sections of this page. Root renders a `nav` named "Table of contents". `List` renders an ordered list, at the top and for nested entries.

## Current section

`active` marks the section in view with `aria-current="location"`: its text turns `semantic.color.text.primary`, and the `Indicator`, a bar on the list's start edge, marks it. When `active` moves to another entry, the indicator slides there and resizes with `semantic.transition.movement.tableOfContents.indicator`, overshooting slightly; instantly when the reader prefers reduced motion. The application decides which section is current, for example with an `IntersectionObserver`. In the demo, choosing an entry makes it current.

```tsx
// active.tsx
import {TableOfContents} from '@earendil-works/elanor-react';
import {useState} from 'react';

const sections = [
  {id: 'usage', title: 'Usage'},
  {id: 'current-section', title: 'Current section'},
  {id: 'nested-sections', title: 'Nested sections'},
  {id: 'api', title: 'API'},
];

export default function TableOfContentsActive() {
  const [active, setActive] = useState('current-section');
  return (
    <TableOfContents.Root>
      <TableOfContents.List>
        {sections.map((section) => (
          <TableOfContents.Item key={section.id}>
            <TableOfContents.Link
              href={`#${section.id}`}
              active={section.id === active}
              onClick={() => setActive(section.id)}
            >
              {section.title}
            </TableOfContents.Link>
          </TableOfContents.Item>
        ))}
      </TableOfContents.List>
      <TableOfContents.Indicator />
    </TableOfContents.Root>
  );
}
```

## Nested sections

Put another `List` inside an `Item`, after its `Link`, for subsections. Nested entries indent only their text, so the bar stays on the same edge.

```tsx
// nested.tsx
import {TableOfContents} from '@earendil-works/elanor-react';

export default function TableOfContentsNested() {
  return (
    <TableOfContents.Root>
      <TableOfContents.List>
        <TableOfContents.Item>
          <TableOfContents.Link href="#usage">Usage</TableOfContents.Link>
        </TableOfContents.Item>
        <TableOfContents.Item>
          <TableOfContents.Link href="#current-section">Current section</TableOfContents.Link>
        </TableOfContents.Item>
        <TableOfContents.Item>
          <TableOfContents.Link href="#nested-sections">Nested sections</TableOfContents.Link>
          <TableOfContents.List>
            <TableOfContents.Item>
              <TableOfContents.Link href="#nested-sections" active>
                Subsections
              </TableOfContents.Link>
            </TableOfContents.Item>
          </TableOfContents.List>
        </TableOfContents.Item>
      </TableOfContents.List>
      <TableOfContents.Indicator />
    </TableOfContents.Root>
  );
}
```

## Menu

For layouts without room for the list beside the content, `Menu` collapses the table of contents into a bar with a dropdown. `MenuTrigger` shows the current section; `MenuPopup` holds the same `List`, `Item`, and `Link` parts, which look like dropdown items there, and choosing a link closes it. The list also closes when the bar's scroll container scrolls. Opening it moves focus to the first link, or to the list when opened by touch, without scrolling the page.

```tsx
// menu.tsx
import {TableOfContents} from '@earendil-works/elanor-react';
import {useState} from 'react';

const sections = [
  {id: 'usage', title: 'Usage'},
  {id: 'current-section', title: 'Current section'},
  {id: 'nested-sections', title: 'Nested sections'},
  {id: 'menu', title: 'Menu'},
];

export default function TableOfContentsMenuDemo() {
  const [active, setActive] = useState('menu');
  const current = sections.find((section) => section.id === active);
  return (
    <TableOfContents.Menu>
      <TableOfContents.MenuTrigger>{current?.title}</TableOfContents.MenuTrigger>
      <TableOfContents.MenuPopup>
        <TableOfContents.List>
          {sections.map((section) => (
            <TableOfContents.Item key={section.id}>
              <TableOfContents.Link
                href={`#${section.id}`}
                active={section.id === active}
                onClick={() => setActive(section.id)}
              >
                {section.title}
              </TableOfContents.Link>
            </TableOfContents.Item>
          ))}
        </TableOfContents.List>
      </TableOfContents.MenuPopup>
    </TableOfContents.Menu>
  );
}
```

```tsx
<TableOfContents.Menu>
  <TableOfContents.MenuTrigger>Current section</TableOfContents.MenuTrigger>
  <TableOfContents.MenuPopup>
    <TableOfContents.List />
  </TableOfContents.MenuPopup>
</TableOfContents.Menu>
```

Keep the trigger's text in sync with the `active` link. Assistive technology hears "Table of contents: " before it; change that with the trigger's `label`, and the list's name with the popup's `label`.

The menu is a bar with separators above and below that sticks to the top of its scroll container, the document or a scrolling ancestor such as a drawer, on `semantic.number.layer.floating`, with a shadow that fades in as content scrolls beneath it. To stick below a header, set `inset-block-start` in your CSS, such as `calc(var(--semantic-dimension-size-top-navigation) - var(--semantic-dimension-border-width-small))` below a top navigation: the bar sticks one border width higher, so the header's border covers its top separator.

## Static rendering

On pages rendered on the server without React in the browser, the behaviors follow the page instead of the application: the static root marks the section in view and moves the `Indicator`, and the static menu shows it in its trigger. Lists, items, links, and the indicator stay the same.

```tsx
<TableOfContents.StaticRoot>
  <TableOfContents.List>…</TableOfContents.List>
  <TableOfContents.Indicator />
</TableOfContents.StaticRoot>

<TableOfContents.StaticMenu>
  <TableOfContents.StaticMenuTrigger>Overview</TableOfContents.StaticMenuTrigger>
  <TableOfContents.StaticMenuPopup>
    <TableOfContents.List>…</TableOfContents.List>
  </TableOfContents.StaticMenuPopup>
</TableOfContents.StaticMenu>
```

```ts
import {
  createPopover,
  createTableOfContents,
  createTableOfContentsMenu,
  enhance,
} from '@earendil-works/elanor-react';

enhance({
  'table-of-contents': createTableOfContents,
  'table-of-contents-menu': createTableOfContentsMenu,
  popover: createPopover,
});
```

A section is in view once its heading reaches the `scroll-padding-top` of the headings' scroll container, the document or a scrolling ancestor, and the last one at the end of it. The menu closes when a link is chosen or the page scrolls, and its bar's shadow fades in as content scrolls beneath it.

## API

### `TableOfContents.Root`

A `nav` named "Table of contents". Put a `List` of `Item`s inside, and an `Indicator` after it to mark the active entry with a bar that moves.

Renders `<nav>`. Accepts its HTML attributes and `ref`.

### `TableOfContents.List`

The ordered list of entries, at the top and nested inside an `Item` after its `Link`.

Renders `<ol>`. Accepts its HTML attributes and `ref`.

### `TableOfContents.Item`

Renders `<li>`. Accepts its HTML attributes and `ref`.

### `TableOfContents.Link`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `active` | `boolean` | `false` | Marks the section in view and sets `aria-current="location"`. |

Renders `<a>`. Also accepts its HTML attributes and `ref`.

### `TableOfContents.Indicator`

A bar that marks the active entry and moves to the next one when `active` changes, like Tabs' indicator. It measures the active link and sets `--active-entry-top` and `--active-entry-height`; it stays hidden until it has measured, and the active entry draws its own bar until then. Place it in Root, after the `List`.

Renders `<span>`. Accepts its HTML attributes, `ref`, and `render`.

### `TableOfContents.Menu`

Renders `<div>`. Accepts its HTML attributes, `ref`, and `render`.

### `TableOfContents.MenuTrigger`

Shows the current section and opens the list.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | - | The current section, shown in the trigger. |
| `label` | `string` | `'Table of contents'` | Read before the current section by assistive technology. |

Also accepts Base UI [`Popover.Trigger`](https://base-ui.com/react/components/popover.md) props.

### `TableOfContents.MenuPopup`

The sections, in a `nav` under the trigger. Put a `List` of `Item`s inside.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | `'Table of contents'` | Accessible name of the popup and the list's `nav`. |

Also accepts HTML attributes and `ref`.

### `TableOfContents.StaticRoot`

A table of contents for pages without React in the browser; `createTableOfContents` marks the section in view.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `data-elanor` | `string` | - | The page's own behaviors for this element, attached after the component's. |

Also accepts HTML attributes and `ref`.

### `TableOfContents.StaticMenu`

The table of contents as a menu in a sticky bar, for pages without React in the browser; `createTableOfContentsMenu`, `createPopover`, and `createTableOfContents` make it work.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `data-elanor` | `string` | - | The page's own behaviors for this element, attached after the component's. |

Renders `<div>`. Also accepts its HTML attributes and `ref`.

### `TableOfContents.StaticMenuTrigger`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | - | The current section, shown in the trigger. |
| `label` | `string` | `'Table of contents'` | Read before the current section by assistive technology. |

Renders `<button>`. Also accepts its HTML attributes and `ref`.

### `TableOfContents.StaticMenuPopup`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | `'Table of contents'` | Accessible name of the popup and the list's `nav`. |
| `data-elanor` | `string` | - | The page's own behaviors for the list's `nav`, attached after the component's. |

Also accepts HTML attributes and `ref`.
