# Table of Contents

> An auto-generated on-page table of contents that scans headings in your content area, builds a clickable nested list, and tracks the active heading as users scroll.

# Table of Contents

## Overview

The Table of Contents component automatically discovers headings in your page content, generates a clickable nested list of anchor links, and highlights the currently visible heading as the user scrolls. It requires no manual link management — drop the component on the page, configure which heading levels to include, and it builds itself at runtime.

The component is a single self-contained element (`OmeTableOfContents`). It is not a family — there are no child blocks to compose. All behavior is controlled through props on the root.

## How It Works

1. **Template phase** — The PHP builder renders a hidden template (``) containing a ``, ``, and `` skeleton. These are cloned at runtime to build the live list.
2. **Target discovery** — The runtime reads `targetSelector` to find the content container. If empty or invalid, it falls back to `main`, `article`, `[role='main']`, `.entry-content`, `.wp-block-post-content`, then `body`. The TOC root itself is always excluded from heading scanning.
3. **Heading extraction** — Headings from H2 through the configured depth are collected. Existing IDs are preserved; headings without IDs receive auto-generated slugs. Duplicate slugs are de-duplicated with numeric suffixes (e.g. `repeated-heading-2`).
4. **Tree building** — Headings are organized into a nested ``/`` tree that mirrors the heading hierarchy.
5. **Active tracking** — An `IntersectionObserver` watches all collected headings. As the user scrolls, the first visible heading's corresponding link receives `aria-current="location"`, and the item gets `data-ome-active`. The root H2 branch item receives `data-ome-active-branch`.
6. **Mutation watching** — A `MutationObserver` watches the target container. If headings are added or removed after load, the TOC automatically rebuilds itself after a short debounce.

## Quick Start

<CodeExample title="Basic — auto-detect headings" language="jsx">
{`<OmeTableOfContents
  structure='{{"tag":"nav"}}'
  content='{{"showLabel":true,"label":"On this page"}}'
  settings='{{"depth":"3","offset":"0"}}'
  targeting='{{"targetSelector":""}}'
  behavior='{{"treeDisplay":"expanded"}}'
  mobile='{{"enabled":false}}'
/>`}
</CodeExample>

This produces a `<nav>` that scans the page for H2–H3 headings, shows an "On this page" label, and highlights the active heading on scroll.

<CodeExample title="Blog post with deep headings, scroll offset, and mobile accordion" language="jsx">
{`<OmeTableOfContents
  structure='{{"tag":"nav"}}'
  content='{{"showLabel":true,"label":"Chapters"}}'
  settings='{{"depth":"4","offset":"96"}}'
  targeting='{{"targetSelector":"article.post-content"}}'
  behavior='{{"treeDisplay":"active-branch"}}'
  mobile='{{"enabled":true,"breakpoint":"768","initialState":"collapsed"}}'
  styling='{{"class":["my-toc"],"labelClass":["my-toc__label"],"branchClass":["my-toc__list"],"itemClass":["my-toc__item"]}}'
  mobile_styling='{{"mobileButtonClass":["my-toc__mobile-btn"],"mobilePanelClass":["my-toc__mobile-panel"],"mobileChevronClass":["my-toc__mobile-chevron"]}}'
/>`}
</CodeExample>

---

## Props

### Structure

<PropsTable group="Structure">
  <Prop name="tag" type="string" defaultValue='"nav"'>
    HTML tag used for the root element. Change to `"div"` if you need a generic container, but `"nav"` is recommended for accessibility since the TOC is a navigation landmark.
  </Prop>
</PropsTable>

### Content

<PropsTable group="Content">
  <Prop name="showLabel" type="boolean" defaultValue="true">
    Shows a label above the generated table of contents. When `false`, no label text is rendered and the list starts immediately.
  </Prop>
  <Prop name="label" type="string" defaultValue='"On this page"'>
    Text shown as the table of contents label. When mobile accordion is enabled, this same text is used as the disclosure button label.
  </Prop>
</PropsTable>

### Settings

<PropsTable group="Settings">
  <Prop name="depth" type={`"2" | "3" | "4" | "5" | "6"`} defaultValue='"3"'>
    Includes H2 through the selected heading level. H1 is always skipped. `"3"` captures H2 and H3 — the most common range for blog posts and documentation. Use `"2"` for a flat H2-only list, or `"4"`–`"6"` for deeply structured content.
  </Prop>
  <Prop name="offset" type="string" defaultValue='"0"'>
    Pixel offset applied to target headings through `scroll-margin-top`. Set this to match your sticky header height so headings land below the header when scrolled to.
  </Prop>
</PropsTable>

### Targeting

<PropsTable group="Targeting">
  <Prop name="targetSelector" type="string" defaultValue='""'>
    CSS selector for the content container to scan for headings. When empty, the runtime falls back to `main`, `article`, `[role='main']`, `.entry-content`, `.wp-block-post-content`, then `body` — whichever it finds first that is not inside the TOC root itself. Set this when your content lives in a specific container and you want to exclude headings from sidebars, headers, or footers.
  </Prop>
</PropsTable>

### Behavior

<PropsTable group="Behavior">
  <Prop name="treeDisplay" type={`"expanded" | "active-branch"`} defaultValue='"expanded"'>
    Controls nested branch visibility. `"expanded"` shows all heading levels at all times. `"active-branch"` reveals nested links only under the currently active H2 branch — all other sub-lists are hidden. Use `"active-branch"` for long pages with many headings to reduce visual noise.
  </Prop>
</PropsTable>

### Mobile

<PropsTable group="Mobile">
  <Prop name="enabled" type="boolean" defaultValue="false">
    Turns the label into a disclosure button below the configured breakpoint. The TOC list collapses into an accordion panel that users can expand and collapse.
  </Prop>
  <Prop name="breakpoint" type="string" defaultValue='"768"'>
    Viewport width in pixels where mobile accordion mode starts. Below this width, the label is replaced with a toggle button and the panel collapses.
  </Prop>
  <Prop name="initialState" type={`"collapsed" | "expanded"`} defaultValue='"collapsed"'>
    Initial mobile accordion state when the page loads below the breakpoint. `"collapsed"` hides the list until the user taps the button. `"expanded"` shows it immediately.
  </Prop>
</PropsTable>

### Styling

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-toc-root-default">
    CSS class applied to the TOC root element.
  </Prop>
  <Prop name="labelClass" type="class" defaultValue="ome-toc-label-default">
    CSS class applied to the label element.
  </Prop>
  <Prop name="branchClass" type="class" defaultValue="ome-toc-branch-default">
    CSS class applied to all generated `` branch lists.
  </Prop>
  <Prop name="itemClass" type="class" defaultValue="ome-toc-item-default">
    CSS class applied to all generated `` items.
  </Prop>
</PropsTable>

### Mobile Styling

<PropsTable group="Mobile Styling">
  <Prop name="mobileButtonClass" type="class" defaultValue="ome-toc-mobile-button-default">
    CSS class applied to the mobile disclosure button (visible only when mobile accordion is enabled and viewport is below the breakpoint).
  </Prop>
  <Prop name="mobilePanelClass" type="class" defaultValue="ome-toc-mobile-panel-default">
    CSS class applied to the mobile disclosure panel that wraps the TOC list in mobile mode.
  </Prop>
  <Prop name="mobileChevronClass" type="class" defaultValue="ome-toc-mobile-chevron-default">
    CSS class applied to the chevron SVG icon inside the mobile disclosure button. The chevron rotates 180 degrees when expanded.
  </Prop>
</PropsTable>

### Advanced Styles

<PropsTable group="Advanced Styles">
  <Prop name="enabled" type="boolean" defaultValue="false">
    Reveals H2–H6 level-specific class controls up to the configured depth. When enabled, you can style each heading level's branches and items independently.
  </Prop>
</PropsTable>

When Advanced Styles is enabled, the following per-level props become available for each heading level up to the configured depth:

| Level | Branch CSS Class | Item CSS Class |
|---|---|---|
| H2 | `ome-toc-h2-branch-default` | `ome-toc-h2-item-default` |
| H3 | `ome-toc-h3-branch-default` | `ome-toc-h3-item-default` |
| H4 | `ome-toc-h4-branch-default` | `ome-toc-h4-item-default` |
| H5 | `ome-toc-h5-branch-default` | `ome-toc-h5-item-default` |
| H6 | `ome-toc-h6-branch-default` | `ome-toc-h6-item-default` |

Each level-specific class sets a `--ome-toc-level` custom property on the element, useful for writing level-aware CSS.

---

## Active State Attributes

The runtime uses **data attributes** (not classes) to mark active state. Target these in your CSS:

| Attribute | Applied to | When |
|---|---|---|
| `data-ome-active` | `` | The currently active heading's list item. |
| `data-ome-active-branch` | `` | The root H2 branch item containing the active heading (only one at a time). |
| `aria-current="location"` | `` | The link pointing to the currently active heading. |

### CSS Example

```css
/* Style the active link */
[data-ome-toc-item][data-ome-active] > a {
  color: var(--primary, #2563eb);
  font-weight: 700;
}

/* Dim inactive branches in active-branch mode */
[data-ome-toc-item]:not([data-ome-active-branch]) {
  opacity: 0.5;
}
```

---

## Mobile Accordion

When `mobile.enabled` is `true`, the component transforms below the breakpoint:

- The static label is replaced with a `<button>` that toggles the TOC list.
- The button includes a chevron icon that rotates when expanded.
- The panel is hidden/shown via the `hidden` attribute.
- ARIA attributes (`aria-expanded`, `aria-controls`) are managed automatically.
- When the viewport crosses back above the breakpoint, the button is removed and the static label is restored.

---

## Heading Discovery Details

### Slug Generation

Headings without an existing `id` attribute receive an auto-generated slug:

1. Text is trimmed, lowercased, and Unicode-normalized (NFKD).
2. Diacritics are stripped.
3. Quotes and apostrophes are removed.
4. Non-alphanumeric characters are replaced with hyphens.
5. Leading/trailing hyphens are removed.
6. If the result is empty, the fallback slug `"section"` is used.

Duplicate IDs are resolved by appending a numeric suffix: `heading`, `heading-2`, `heading-3`, etc. Existing IDs on headings are always preserved.

### Content Container Fallbacks

When `targetSelector` is empty or does not match a valid element outside the TOC root, the runtime tries these selectors in order:

1. `main`
2. `article`
3. `[role='main']`
4. `.entry-content`
5. `.wp-block-post-content`
6. `body`

The first element found that is **not inside** the TOC root is used as the heading source.

---

## Common Mistakes

<CommonMistake title="Placing the TOC inside the content container it scans">
  If the TOC root is inside the element it targets for headings, the runtime skips it correctly — but the `targetSelector` must resolve to an element outside the TOC root. If your selector accidentally targets the TOC itself, no headings will be found and the component hides itself. Use a specific content selector (like `article.post-content`) when the TOC is nested inside a shared layout.
</CommonMistake>

<CommonMistake title="Expecting active classes instead of data attributes">
  Active state is communicated through `data-ome-active`, `data-ome-active-branch`, and `aria-current="location"` — not CSS classes. Do not write selectors like `.is-active` or `.active-link`. Use `[data-ome-active] > a` in your CSS instead.
</CommonMistake>

<CommonMistake title="Setting depth to include H1">
  H1 is always excluded. The minimum heading level is H2. Setting `depth` to `"2"` produces a flat list of H2 headings only — no nesting.
</CommonMistake>

<CommonMistake title="Forgetting the scroll offset with a sticky header">
  If your site has a sticky header, headings will scroll behind it when the user clicks a TOC link. Set `offset` to your header height (e.g. `"96"`) so `scroll-margin-top` pushes the heading below the header.
</CommonMistake>

---

## FAQs

<details>
<summary>What happens when there are no headings?</summary>

The TOC root is hidden (`hidden` attribute) and no links are rendered. It becomes completely invisible — no empty container or label is shown. If headings are later added via JavaScript (e.g. dynamic content), the MutationObserver triggers a rebuild and the TOC reappears automatically.

</details>

<details>
<summary>Can I have multiple TOC components on the same page?</summary>

Yes. Each TOC instance manages its own heading collection, active tracking, and mobile accordion independently. Use `targetSelector` on each instance to point to different content containers so they don't scan each other's headings.

</details>

<details>
<summary>How does the component handle duplicate heading text?</summary>

Headings with identical text receive unique auto-generated IDs: the first gets `some-heading`, the second `some-heading-2`, and so on. Headings that already have an `id` attribute keep that ID unchanged — even if it duplicates another heading's slug.

</details>

<details>
<summary>Does the TOC update when content changes dynamically?</summary>

Yes. A `MutationObserver` watches the target container's subtree. When headings are added or removed, the TOC rebuilds itself after a 50ms debounce. The IntersectionObserver for active tracking is also re-created during the rebuild.

</details>

<details>
<summary>How do I style each heading level differently?</summary>

Enable **Advanced Styles** (`advanced_styles.enabled = true`), then set per-level branch and item classes. Each level's default class sets `--ome-toc-level` on the element. You can use this custom property to write level-aware CSS:

```css
[data-ome-toc-item] {
  padding-inline-start: calc((var(--ome-toc-level, 2) - 2) * 1rem);
}
```

</details>

<details>
<summary>Can I use the TOC without the label?</summary>

Yes. Set `content.showLabel` to `false`. The list renders without any label text. If mobile accordion is enabled, the disclosure button uses the `aria-label` on the root nav element instead.

</details>
