---
title: "Table of Contents"
description: "The Table of Contents (TOC) subsystem in Fumadocs is a highly reactive, scroll-aware navigation component designed to synchronize with the page's DOM state. It bridges the gap between static conten..."
last_updated: "2026-07-02T09:46:38.94724+00:00"
canonical_url: "https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/rendering-ui/table-of-contents"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [packages/core/src/toc.tsx](https://github.com/blade47/fumadocs/blob/main/packages/core/src/toc.tsx)
- [packages/base-ui/src/components/toc/default.tsx](https://github.com/blade47/fumadocs/blob/main/packages/base-ui/src/components/toc/default.tsx)
- [packages/radix-ui/src/components/toc/default.tsx](https://github.com/blade47/fumadocs/blob/main/packages/radix-ui/src/components/toc/default.tsx)
- [packages/base-ui/src/components/toc/clerk.tsx](https://github.com/blade47/fumadocs/blob/main/packages/base-ui/src/components/toc/clerk.tsx)
- [packages/radix-ui/src/components/toc/clerk.tsx](https://github.com/blade47/fumadocs/blob/main/packages/radix-ui/src/components/toc/clerk.tsx)
- [packages/base-ui/src/layouts/notebook/page/slots/toc.tsx](https://github.com/blade47/fumadocs/blob/main/packages/base-ui/src/layouts/notebook/page/slots/toc.tsx)
- [packages/radix-ui/src/layouts/notebook/page/slots/toc.tsx](https://github.com/blade47/fumadocs/blob/main/packages/radix-ui/src/layouts/notebook/page/slots/toc.tsx)
- [packages/base-ui/src/layouts/home/slots/header.tsx](https://github.com/blade47/fumadocs/blob/main/packages/base-ui/src/layouts/home/slots/header.tsx)
- [packages/core/src/mdx-plugins/remark-structure.ts](https://github.com/blade47/fumadocs/blob/main/packages/core/src/mdx-plugins/remark-structure.ts)
- [packages/preview/src/pages/[...slugs].tsx](https://github.com/blade47/fumadocs/blob/main/packages/preview/src/pages/%5B...slugs%5D.tsx)
- [packages/core/src/mdx-plugins/rehype-toc.ts](https://github.com/blade47/fumadocs/blob/main/packages/core/src/mdx-plugins/rehype-toc.ts)
- [packages/radix-ui/src/utils/use-footer-items.ts](https://github.com/blade47/fumadocs/blob/main/packages/radix-ui/src/utils/use-footer-items.ts)
- [packages/base-ui/src/components/toc/index.tsx](https://github.com/blade47/fumadocs/blob/main/packages/base-ui/src/components/toc/index.tsx)
- [packages/openapi/src/ui/components/heading.tsx](https://github.com/blade47/fumadocs/blob/main/packages/openapi/src/ui/components/heading.tsx)
- [packages/radix-ui/src/components/toc/index.tsx](https://github.com/blade47/fumadocs/blob/main/packages/radix-ui/src/components/toc/index.tsx)
- [packages/asyncapi/src/ui/components/heading.tsx](https://github.com/blade47/fumadocs/blob/main/packages/asyncapi/src/ui/components/heading.tsx)
- [packages/api-docs/src/components/select-tab.tsx](https://github.com/blade47/fumadocs/blob/main/packages/api-docs/src/components/select-tab.tsx)
- [packages/basehub/src/toc.tsx](https://github.com/blade47/fumadocs/blob/main/packages/basehub/src/toc.tsx)
- [packages/base-ui/src/layouts/flux/page/slots/toc.tsx](https://github.com/blade47/fumadocs/blob/main/packages/base-ui/src/layouts/flux/page/slots/toc.tsx)
- [packages/radix-ui/src/layouts/flux/page/slots/toc.tsx](https://github.com/blade47/fumadocs/blob/main/packages/radix-ui/src/layouts/flux/page/slots/toc.tsx)
- [packages/core/src/source/llms.ts](https://github.com/blade47/fumadocs/blob/main/packages/core/src/source/llms.ts)
- [packages/core/src/mdx-plugins/remark-heading.ts](https://github.com/blade47/fumadocs/blob/main/packages/core/src/mdx-plugins/remark-heading.ts)
- [packages/radix-ui/src/layouts/home/slots/header.tsx](https://github.com/blade47/fumadocs/blob/main/packages/radix-ui/src/layouts/home/slots/header.tsx)
- [packages/base-ui/src/components/heading.tsx](https://github.com/blade47/fumadocs/blob/main/packages/base-ui/src/components/heading.tsx)
- [packages/openapi/src/utils/pages/to-static-data.ts](https://github.com/blade47/fumadocs/blob/main/packages/openapi/src/utils/pages/to-static-data.ts)
- [packages/radix-ui/src/components/heading.tsx](https://github.com/blade47/fumadocs/blob/main/packages/radix-ui/src/components/heading.tsx)
- [packages/sanity/src/client.ts](https://github.com/blade47/fumadocs/blob/main/packages/sanity/src/client.ts)
- [packages/base-ui/src/components/inline-toc.tsx](https://github.com/blade47/fumadocs/blob/main/packages/base-ui/src/components/inline-toc.tsx)
- [packages/radix-ui/src/components/inline-toc.tsx](https://github.com/blade47/fumadocs/blob/main/packages/radix-ui/src/components/inline-toc.tsx)
- [packages/base-ui/src/layouts/notebook/slots/header.tsx](https://github.com/blade47/fumadocs/blob/main/packages/base-ui/src/layouts/notebook/slots/header.tsx)
</details>

The Table of Contents (TOC) subsystem in Fumadocs is a highly reactive, scroll-aware navigation component designed to synchronize with the page's DOM state. It bridges the gap between static content generation (MDX parsing) and dynamic UI feedback, allowing users to track their current position in long-form documentation seamlessly.

The subsystem operates through a multi-layered architecture: a core observation layer that detects intersection changes, a transformation layer that translates these observations into visual state, and a UI layer that maps headings to interactive navigation items. By utilizing `IntersectionObserver` coupled with a custom notification system, the TOC ensures high-performance tracking without manual scroll listeners.

This system effectively solves the "floating navigation" problem in complex technical documentation, where users need context-aware markers that stay updated as they navigate. Its modularity permits various visual implementations (like the "clerk" style vs. the "default" style) while relying on a unified set of reactive primitives for the underlying logic.

## Observation Logic and Reactive State

The core of the TOC logic resides in the `Observer` class located in `packages/core/src/toc.tsx`. This class manages the state of all tracked items and orchestrates the `IntersectionObserver` instances. When `setItems` is called, it reconciles the provided items list with the DOM, clearing existing observers and initializing new ones on the corresponding heading elements.

The `callback` method is the heart of the synchronization loop. It processes incoming intersection entries, maintaining an `active` state for each heading. The system handles scenarios where no headings are intersecting by defaulting to the nearest heading in the viewport, which prevents the TOC from showing an empty state during rapid scrolling.

```mermaid
flowchart TD
    A[IntersectionObserver Callback] --> B{Entries Exist?}
    B -- Yes --> C[Update items<br>active state]
    C --> D{Has Active Item?}
    D -- No --> E[Calculate<br>closest heading<br>as fallback]
    E --> F[Update<br>State]
    D -- Yes --> F
```
Sources: [packages/core/src/toc.tsx:282-331](https://github.com/blade47/fumadocs/blob/main/packages/core/src/toc.tsx#L282-L331)

> [!NOTE]
> The `Observer` class uses a `Map`-like approach internally to maintain a `Set` of `ChangeListener` listeners. Whenever the internal item list changes via `update`, all registered listeners are notified, enabling UI components to re-render reactively.

## The UI Transformation Pipeline

While the `core` package handles state, UI packages (like `base-ui` and `radix-ui`) transform this state into visual representation using SVGs and CSS variables. The `TOCItems` component calculates the layout of the TOC path, creating a dynamic visual "track" that follows the current scroll position.

The calculation logic in `default.tsx` maps the nesting depth of headings (`depth`) to horizontal offsets, ensuring a tree-like visual indentation. These offsets are derived using `getLineOffset` and `getItemOffset` functions, which transform the hierarchical document structure into a renderable SVG path.

| Function | Purpose | Input |
| :--- | :--- | :--- |
| `getLineOffset` | Returns horizontal starting position for lines | `depth: number` |
| `getItemOffset` | Returns horizontal padding for text items | `depth: number` |

Sources: [packages/base-ui/src/components/toc/default.tsx:240-250](https://github.com/blade47/fumadocs/blob/main/packages/base-ui/src/components/toc/default.tsx#L240-L250)

## Context-Based Integration

The TOC subsystem relies on `React Context` to expose the observer and the scroll state to nested items. The `AnchorProvider` wraps the content, providing an `ObserverContext`, while the `ScrollProvider` provides the `containerRef`, necessary for the `scrollIntoView` logic when a user clicks an item.

```mermaid
sequenceDiagram
    participant P as TOCProvider
    participant O as AnchorProvider
    participant I as TOCItem
    P->>O: Provide TOC items
    O->>O: Initialize Observer
    O->>I: Provide Observer via Context
    I->>I: Watch for intersection
    I->>I: Trigger Auto-scroll on active
```
Sources: [packages/core/src/toc.tsx:38-83](https://github.com/blade47/fumadocs/blob/main/packages/core/src/toc.tsx#L38-L83)

## Static Data and MDX Integration

To support static generation or server-side rendering contexts (like `remark`), the subsystem includes plugins such as `remark-structure` and `rehype-toc`. These plugins parse the MDX tree to extract headings and their metadata *before* the component is even rendered on the client.

The `rehype-toc` plugin specifically scans heading tags and generates an array of items, which can then be exported as an ESM variable (`toc`) or embedded in the file's metadata. This ensures that the TOC doesn't require a full re-parse on every client render if the structure is known at build time.

> [!IMPORTANT]
> The `rehype-toc` plugin requires `hProperties.id` to be present on headings. If headings lack IDs, the TOC logic cannot reliably track them; `remark-heading` should be used as a prerequisite plugin to ensure every heading has a stable, slugified ID.

Sources: [packages/core/src/mdx-plugins/rehype-toc.ts:55-108](https://github.com/blade47/fumadocs/blob/main/packages/core/src/mdx-plugins/rehype-toc.ts#L55-L108)

## Design Trade-offs

The architecture reflects a preference for performance and visual flexibility over a single-component solution.

| Design Choice | Benefit | Cost |
| :--- | :--- | :--- |
| `IntersectionObserver` | High performance, avoids main-thread scroll jank | Requires consistent element IDs |
| SVG-based track lines | Fluid visual connections and step markers | Complexity in calculating SVG offsets |
| Reactive Context-based API | Decoupled UI and Logic, supports multiple layouts | Requires nested providers |

Sources: [packages/core/src/toc.tsx:228-346](https://github.com/blade47/fumadocs/blob/main/packages/core/src/toc.tsx#L228-L346)

## Implementation Example: Integrating the TOC

To use the TOC in a document layout, wrap the content in the `TOCProvider` and render the `TOC` component. The following snippet illustrates how a developer integrates the TOC into a layout:

```tsx
import { TOCProvider, TOC } from 'fumadocs-ui/components/toc';

export function DocsPage({ toc, children }) {
  return (
    <TOCProvider toc={toc}>
      <main>{children}</main>
      <TOC />
    </TOCProvider>
  );
}
```
Sources: [packages/preview/src/pages/[...slugs].tsx:212-221](https://github.com/blade47/fumadocs/blob/main/packages/preview/src/pages/%5B...slugs%5D.tsx#L212-L221)

> [!TIP]
> Always place the `TOCProvider` as high as possible in the document component tree to ensure all `TOCItem` components receive the updated context immediately upon mount.

## Related

- [Layout System](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/rendering-ui/layout-system)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/llms.txt) for all pages in this wiki.
