---
title: "Table of Contents"
description: "Fumadocs provides a table of contents (TOC) architecture that extracts heading hierarchies from Markdown ASTs, headless CMS content, and OpenAPI sources, and renders interactive, synchronized navig..."
last_updated: "2026-10-12T07:53:59.981993+00:00"
canonical_url: "https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/table-of-contents"
---

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

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

- [packages/base-ui/src/layouts/notebook/slots/sidebar.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/notebook/slots/sidebar.tsx)
- [packages/radix-ui/src/layouts/notebook/slots/sidebar.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/notebook/slots/sidebar.tsx)
- [packages/core/src/toc.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/toc.tsx)
- [packages/base-ui/src/components/toc/clerk.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/clerk.tsx)
- [packages/base-ui/src/components/toc/default.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/default.tsx)
- [packages/radix-ui/src/components/toc/default.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/toc/default.tsx)
- [packages/base-ui/src/layouts/flux/page/slots/toc.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/flux/page/slots/toc.tsx)
- [packages/radix-ui/registry/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/registry/index.ts)
- [packages/radix-ui/src/components/toc/clerk.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/toc/clerk.tsx)
- [packages/base-ui/src/layouts/notebook/page/slots/toc.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/notebook/page/slots/toc.tsx)
- [packages/core/src/mdx-plugins/rehype-toc.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-toc.ts)
- [packages/radix-ui/src/layouts/flux/page/slots/toc.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/flux/page/slots/toc.tsx)
- [packages/base-ui/registry/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/registry/index.ts)
- [packages/radix-ui/src/layouts/notebook/page/slots/toc.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/notebook/page/slots/toc.tsx)
- [packages/api-docs/tsdown.config.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/api-docs/tsdown.config.ts)
- [packages/base-ui/tsdown.config.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/tsdown.config.ts)
- [packages/radix-ui/tsdown.config.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/tsdown.config.ts)
- [packages/base-ui/src/components/toc/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/index.tsx)
- [packages/preview/src/pages/...slugs.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx)
- [packages/base-ui/src/layouts/flux/slots/sidebar.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/flux/slots/sidebar.tsx)
- [packages/radix-ui/src/components/toc/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/toc/index.tsx)
- [packages/core/src/mdx-plugins/remark-structure.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-structure.ts)
- [packages/core/src/mdx-plugins/remark-heading.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-heading.ts)
- [packages/basehub/src/toc.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/basehub/src/toc.tsx)
- [packages/radix-ui/src/components/inline-toc.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/inline-toc.tsx)
- [packages/base-ui/src/components/inline-toc.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/inline-toc.tsx)
- [packages/core/src/content/toc.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/content/toc.ts)
- [packages/openapi/src/utils/pages/to-static-data.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-static-data.ts)
- [packages/sanity/src/client.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/src/client.ts)
- [packages/base-ui/src/contexts/tree.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/contexts/tree.tsx)
</details>

## Overview

Fumadocs provides a table of contents (TOC) architecture that extracts heading hierarchies from Markdown ASTs, headless CMS content, and OpenAPI sources, and renders interactive, synchronized navigation lists across documentation layouts. The system handles active heading observation via `IntersectionObserver`, drives scroll-into-view behavior, and powers dynamic SVG thumb indicators and collapsible inline elements.

Sources: [packages/core/src/toc.tsx:1-237](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/toc.tsx#L1-L237), [packages/core/src/mdx-plugins/remark-heading.ts:1-99](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-heading.ts#L1-L99), [packages/base-ui/src/components/toc/default.tsx:30-143](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/default.tsx#L30-L143)

## TOC extraction and data structures

### Data Models and Core Types

The foundational data model for table of contents entries in Fumadocs is defined by the `TOCItemType` interface, which represents an individual heading or navigation link. A complete table of contents collection is typed as `TableOfContents`, representing an array of these items.

```typescript
export interface TOCItemType {
  title: ReactNode;
  url: string;
  depth: number;
  /** [remark-steps] the step number */
  _step?: number;
}

export type TableOfContents = TOCItemType[];
```

Sources: [packages/core/src/toc.tsx:18-26](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/toc.tsx#L18-L26)

Additionally, runtime tracking wraps these static items into `TOCItemInfo` objects to manage intersection state, timestamps, and viewport fallbacks.

| Property | Type | Default | Purpose |
| :--- | :--- | :--- | :--- |
| `id` | `string` | required | Unique DOM identifier extracted from the URL anchor |
| `active` | `boolean` | required | Indicates whether the heading is currently active in the viewport |
| `t` | `number` | required | Timestamp of the last time the item state was updated |
| `fallback` | `boolean` | required | Flags items that are currently active but not actively intersecting in the viewport |
| `original` | `TOCItemType` | required | Reference to the underlying static TOC item definition |

Sources: [packages/core/src/toc.tsx:28-36](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/toc.tsx#L28-L36)

> [!NOTE]
> The `_step` optional property on `TOCItemType` allows remark-steps plugins to attach sequential numbering directly to TOC nodes, enabling specialized step-based navigation renderers.

Sources: [packages/core/src/toc.tsx:18-24](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/toc.tsx#L18-L24)

### Markdown AST Extraction Pipeline

Table of contents extraction from Markdown and MDX documents is driven by the remark ecosystem via `getTableOfContents`. The extraction function accepts content and optional remark plugins, processing them synchronously or asynchronously while applying `remarkHeading` to harvest heading nodes from the syntax tree.

```typescript
export function getTableOfContents(
  content: Compatible,
  remarkPlugins?: PluggableList,
): TOCItemType[] | Promise<TOCItemType[]> {
  if (remarkPlugins) {
    return remark()
      .use(remarkPlugins)
      .use(remarkHeading)
      .process(content)
      .then((result) => {
        if ('toc' in result.data) return result.data.toc as TOCItemType[];

        return [];
      });
  }

  const result = remark().use(remarkHeading).processSync(content);

  if ('toc' in result.data) return result.data.toc as TOCItemType[];
  return [];
}
```

Sources: [packages/core/src/content/toc.ts:25-46](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/content/toc.ts#L25-L46)

> [!WARNING]
> If the processed VFile `result.data` object does not contain a `toc` property populated by `remarkHeading`, `getTableOfContents` silently returns an empty array instead of throwing an error.

Sources: [packages/core/src/content/toc.ts:34-45](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/content/toc.ts#L34-L45)

### CMS and OpenAPI Source Adapters

Fumadocs also normalizes table of contents data structures from external sources such as Sanity CMS and OpenAPI specifications. The `renderToc` utility transforms Portable Text blocks into standard `TOCItemType` entries by mapping block levels, generating anchor keys, and rendering block titles.

```typescript
export function renderToc(opts: {
  toc: PortableTextBlock[];
  render: (body: PortableTextBlock) => ReactNode;
}): TOCItemType[] {
  const { render, toc } = opts;

  return toc.map(
    (item): TOCItemType => ({
      depth: Number(item.level ?? 0),
      title: render({ ...item, style: undefined }),
      url: `#${item._key}`,
    }),
  );
}
```

Sources: [packages/sanity/src/client.ts:5-18](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/src/client.ts#L5-L18)

For API documentation generated from OpenAPI schemas, `toStaticData` iterates through page operations and webhooks, building structured headings and table of contents entries using GitHub sluggers.

```typescript
export function toStaticData(
  page: GeneratedPageProps,
  doc: Document,
): {
  toc: TOCItemType[];
  structuredData: StructuredData;
} {
  const slugger = new Slugger();
  const toc: TOCItemType[] = [];
  const structuredData: StructuredData = { headings: [], contents: [] };

  function pathItem(item: OperationObject, defaultTitle: string) {
    if (page.showTitle && item.operationId) {
      const title = item.summary || (item.operationId ? idToTitle(item.operationId) : defaultTitle);
      const id = slugger.slug(title);

      toc.push({
        depth: 2,
        title,
        url: `#${id}`,
      });
      structuredData.headings.push({
        content: title,
        id,
      });
    }
```

Sources: [packages/openapi/src/utils/pages/to-static-data.ts:9-35](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-static-data.ts#L9-L35)

## Active heading observation primitives

### Active Anchor Hooks and Selectors

Core observer mechanisms, scroll listeners, and active anchor tracking hooks manage the runtime intersection of document headings. The `Observer` class maintains the collection of items, observer instances, and change listeners, while React hooks wrap state subscriptions and auto-scroll behaviors.

```mermaid
sequenceDiagram
    participant TOCItem as TOCItem
    participant useTOCListener as useTOCListener
    participant useObserver as useObserver
    participant Observer as Observer
    TOCItem->>TOCItem: useTOCListener
    TOCItem->>useTOCListener: useTOCListener
    useTOCListener->>useObserver: useObserver
    useObserver->>Observer: observer.listen(callback)
```

Sources: [packages/core/src/toc.tsx:122-130](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/toc.tsx#L122-L130), [packages/core/src/toc.tsx:140-144](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/toc.tsx#L140-L144), [packages/core/src/toc.tsx:162-170](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/toc.tsx#L162-L170)

The observation lifecycle begins within `AnchorProvider`, which instantiates a single `Observer` via `useMemo` and configures its single-item mode and intersection threshold.

```typescript
export function AnchorProvider({ toc, single = false, children }: AnchorProviderProps) {
  const observer = useMemo(() => new Observer(), []);

  observer.single = single;
  useEffect(() => {
    observer.setItems(toc);
  }, [observer, toc]);

  useEffect(() => {
    observer.watch({
      threshold: 0.9,
    });

    return () => observer.unwatch();
  }, [observer]);

  return <ObserverContext value={observer}>{children}</ObserverContext>;
}
```

Sources: [packages/core/src/toc.tsx:66-83](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/toc.tsx#L66-L83)

> [!CAUTION]
> Attempting to use any table of contents hook or `TOCItem` outside of an active `<AnchorProvider />` component will trigger an invariant error thrown by `useObserver()`.

Sources: [packages/core/src/toc.tsx:140-144](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/toc.tsx#L140-L144)

The `useTOCSelector` primitive subscribes components to state updates emitted by the `Observer` instance, running selector functions and memoizing equality checks via `isEqualShallow`. Specific hooks like `useActiveAnchor`, `useActiveAnchors`, and `useItems` derive targeted state views from the primary item collection.

| Hook Name | Return Type | Selection Logic |
| :--- | :--- | :--- |
| `useActiveAnchor()` | `string \| undefined` | Returns the ID of the most recently updated active heading (`item.t > out.t`) |
| `useActiveAnchors()` | `string[]` | Collects an array of all IDs where `item.active` evaluates to true |
| `useItems()` | `TOCItemInfo[]` | Returns the complete array of `TOCItemInfo` tracking items |

Sources: [packages/core/src/toc.tsx:172-219](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/toc.tsx#L172-L219)

> [!TIP]
> When implementing custom sidebar highlighting or custom rendering widgets, prefer `useTOCSelector` with a custom equality function to prevent unnecessary React re-renders on every scroll tick.

Sources: [packages/core/src/toc.tsx:172-184](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/toc.tsx#L172-L184)

## TOC component hierarchy and providers

### Core and UI Provider Layer

The architecture divides table of contents management into core calculation logic in `fumadocs-core/toc` and framework-specific wrapper components in `fumadocs-ui/base-ui` and `fumadocs-ui/radix-ui`. Both UI packages re-export identical provider structures and scroll area primitives that bridge `Primitive.AnchorProvider` with React context propagation for static item arrays.

```tsx
const TOCContext = createContext<Primitive.TOCItemType[]>([]);

export function useTOCItems(): Primitive.TOCItemType[] {
  return use(TOCContext);
}

export type TOCProviderProps = Primitive.AnchorProviderProps;

export const { useActiveAnchor, useActiveAnchors, useItems } = Primitive;

export function TOCProvider({ toc, children, ...props }: TOCProviderProps) {
  return (
    <TOCContext value={toc}>
      <Primitive.AnchorProvider toc={toc} {...props}>
        {children}
      </Primitive.AnchorProvider>
    </TOCContext>
  );
}
```

Sources: [packages/base-ui/src/components/toc/index.tsx:7-25](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/index.tsx#L7-L25), [packages/radix-ui/src/components/toc/index.tsx:7-25](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/toc/index.tsx#L7-L25)

The `TOCProvider` component accepts `toc` items and optional properties such as `single`, injecting both a local `TOCContext` containing raw items and a `Primitive.AnchorProvider` context down the React component tree.

```typescript
export function TOCProvider({ toc, children, ...props }: TOCProviderProps) {
  return (
    <TOCContext value={toc}>
      <Primitive.AnchorProvider toc={toc} {...props}>
        {children}
      </Primitive.AnchorProvider>
    </TOCContext>
  );
}
```

Sources: [packages/base-ui/src/components/toc/index.tsx:17-25](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/index.tsx#L17-L25), [packages/radix-ui/src/components/toc/index.tsx:17-25](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/toc/index.tsx#L17-L25)

> [!NOTE]
> `useTOCItems()` consumes `TOCContext` to retrieve the static `TOCItemType[]` array directly, whereas core hooks like `useItems()` derive state from the active `Observer` instance.

Sources: [packages/base-ui/src/components/toc/index.tsx:7-11](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/index.tsx#L7-L11), [packages/radix-ui/src/components/toc/index.tsx:7-11](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/toc/index.tsx#L7-L11)

### Scroll Areas and Context Binding

The layout incorporates `TOCScrollArea` to manage container-based auto-scrolling when active headings change. It establishes a `viewRef` reference on the scrollable container div and forwards it to `Primitive.ScrollProvider`.

```tsx
export function TOCScrollArea({ ref, className, ...props }: ComponentProps<'div'>) {
  const viewRef = useRef<HTMLDivElement>(null);

  return (
    <div
      ref={mergeRefs(viewRef, ref)}
      className={cn(
        'relative min-h-0 text-sm ms-px overflow-auto [scrollbar-width:none] mask-[linear-gradient(to_bottom,transparent,white_16px,white_calc(100%-16px),transparent)] py-3',
        className,
      )}
      {...props}
    >
      <Primitive.ScrollProvider containerRef={viewRef}>{props.children}</Primitive.ScrollProvider>
    </div>
  );
```

Sources: [packages/base-ui/src/components/toc/index.tsx:27-41](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/index.tsx#L27-L41), [packages/radix-ui/src/components/toc/index.tsx:27-41](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/toc/index.tsx#L27-L41)

The table below lists the components, providers, and shared hooks exported across the core and UI packages:

| Identifier | Source Package | Role & Description |
| :--- | :--- | :--- |
| `AnchorProvider` | `fumadocs-core/toc` | Initializes the `Observer` class, manages single-item selection modes, and supplies `ObserverContext` |
| `ScrollProvider` | `fumadocs-core/toc` | Supplies `ScrollContext` with the container reference for auto-scrolling active items into view |
| `TOCProvider` | `fumadocs-ui/*` | Combines `TOCContext` array storage with `Primitive.AnchorProvider` wrapping |
| `TOCScrollArea` | `fumadocs-ui/*` | Renders the styled container (`overflow-auto`, `[scrollbar-width:none]`, mask gradients) hosting `ScrollProvider` |
| `useTOCItems()` | `fumadocs-ui/*` | Accesses the static array of `TOCItemType` objects from `TOCContext` |

Sources: [packages/core/src/toc.tsx:61-83](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/toc.tsx#L61-L83), [packages/base-ui/src/components/toc/index.tsx:7-41](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/index.tsx#L7-L41), [packages/radix-ui/src/components/toc/index.tsx:7-41](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/toc/index.tsx#L7-L41)

> [!WARNING]
> `TOCScrollArea` applies `[scrollbar-width:none]` and vertical CSS mask gradients (`mask-[linear-gradient(to_bottom,transparent,white_16px,white_calc(100%-16px),transparent)]`); overriding overflow properties without preserving the container reference will break `scrollIntoView` auto-centering.

Sources: [packages/base-ui/src/components/toc/index.tsx:31-40](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/index.tsx#L31-L40), [packages/radix-ui/src/components/toc/index.tsx:31-40](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/toc/index.tsx#L31-L40)

## Indicator thumb tracking and rendering

### SVG Layout Calculations and Path Generation

```mermaid
sequenceDiagram
    participant TOCItems as TOCItems
    participant ThumbTrack as ThumbTrack
    participant useTOC as useTOC
    participant useObserver as useObserver
    TOCItems->>ThumbTrack: compute SVG and render component
    ThumbTrack->>useTOC: call tocInfo.get()
    useTOC->>useObserver: read observer.items
```

Sources: [packages/base-ui/src/components/toc/clerk.tsx:28-126](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/clerk.tsx#L28-L126), [packages/base-ui/src/components/toc/clerk.tsx:137-185](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/clerk.tsx#L137-L185), [packages/core/src/toc.tsx:147-160](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/toc.tsx#L147-L160), [packages/core/src/toc.tsx:140-144](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/toc.tsx#L140-L144)

The table of contents indicator renders dynamic SVG track lines alongside active item thumbs using layout measurements derived from container DOM queries. Within `TOCItems`, an `onPrint` callback measures each item element via `container.querySelector('a[href="' + item.url + '"]')` and computes layout coordinates.

```tsx
for (let i = 0; i < items.length; i++) {
  const item = items[i];
  const element: HTMLElement | null = container.querySelector(`a[href="${item.url}"]`);
  if (!element) continue;

  const styles = getComputedStyle(element);
  const x = getLineOffset(item.depth) + 0.5;
  const top = element.offsetTop + parseFloat(styles.paddingTop);
  const bottom = element.offsetTop + element.clientHeight - parseFloat(styles.paddingBottom);

  w = Math.max(x + 8, w);
  h = Math.max(h, bottom);

  if (i === 0) {
    d += ` M${x} ${top} L${x} ${bottom}`;
  } else {
    const [, upperBottom, upperX] = i > 0 ? positions[i - 1] : [0, 0, 0];

    d += ` C ${upperX} ${top - 4} ${x} ${upperBottom + 4} ${x} ${top} L${x} ${bottom}`;
  }

  if (item._step !== undefined) {
    output.push(
      <g key={i} transform={`translate(${x}, ${(top + bottom) / 2})`}>
        <circle cx="0" cy="0" r="8" className="fill-fd-primary" />
        <text
          cx="0"
          cy="0"
          textAnchor="middle"
          alignmentBaseline="central"
          dominantBaseline="middle"
          className="fill-fd-primary-foreground font-medium text-xs leading-none font-mono rtl:-scale-x-100"
        >
          {item._step}
        </text>
      </g>,
    );
  }

  positions.push([top, bottom, x]);
}
```

Sources: [packages/base-ui/src/components/toc/default.tsx:48-88](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/default.tsx#L48-L88), [packages/radix-ui/src/components/toc/default.tsx:48-88](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/toc/default.tsx#L48-L88)

> [!NOTE]
> The default variant path uses cubic bezier commands to smoothly bridge line segments across varying heading depths, whereas the Clerk variant employs straight line segments.

Sources: [packages/base-ui/src/components/toc/default.tsx:63-67](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/default.tsx#L63-L67), [packages/base-ui/src/components/toc/clerk.tsx:61-65](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/clerk.tsx#L61-L65), [packages/radix-ui/src/components/toc/default.tsx:63-67](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/toc/default.tsx#L63-L67), [packages/radix-ui/src/components/toc/clerk.tsx:61-65](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/toc/clerk.tsx#L61-L65)

### Thumb Track Clip Paths and Variants

The `ThumbTrack` subcomponent reads active states via `Primitive.useTOC()` and `Primitive.useTOCListener()`, dynamically updating CSS custom properties `--track-top` and `--track-bottom` on the container element.

```tsx
function ThumbTrack({ computed }: { computed: ComputedSVG }) {
  const ref = useRef<HTMLDivElement>(null);
  const tocInfo = Primitive.useTOC();

  function calculate(items: Primitive.TOCItemInfo[]) {
    const out: Record<string, string> = {};
    const startIdx = items.findIndex((item) => item.active);
    if (startIdx === -1) return out;

    const endIdx = items.findLastIndex((item) => item.active);
    out['--track-top'] = `${computed.positions[startIdx][0]}px`;
    out['--track-bottom'] = `${computed.positions[endIdx][1]}px`;
    return out;
  }

  Primitive.useTOCListener((items) => {
    const element = ref.current;
    if (!element) return;

    for (const [k, v] of Object.entries(calculate(items))) {
      element.style.setProperty(k, v);
    }
  });

  return (
    <div
      ref={ref}
      className="absolute top-0 inset-s-0 origin-center rtl:-scale-x-100"
      style={{
        width: computed.width,
        height: computed.height,
        ...calculate(tocInfo.get()),
      }}
    >
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox={`0 0 ${computed.width} ${computed.height}`}
        className="absolute transition-[clip-path]"
        style={{
          width: computed.width,
          height: computed.height,
          clipPath: `polygon(0 var(--track-top,0), 100% var(--track-top,0), 100% var(--track-bottom,0), 0 var(--track-bottom,0))`,
        }}
      >
        {computed.content}
      </svg>
    </div>
  );
}
```

Sources: [packages/base-ui/src/components/toc/clerk.tsx:137-185](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/clerk.tsx#L137-L185), [packages/radix-ui/src/components/toc/clerk.tsx:137-185](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/toc/clerk.tsx#L137-L185)

| Variant Package / File | Computed SVG Feature | Path Command / Styling Method | Step Numbering Support |
| :--- | :--- | :--- | :--- |
| `packages/base-ui/src/components/toc/default.tsx` | Cubic path smoothing with optional `thumbBox` lengths | Cubic bezier path calculation | Yes (`item._step !== undefined`) |
| `packages/base-ui/src/components/toc/clerk.tsx` | Straight line segment transitions with polygon clipping | Straight line segment path calculation | Yes (`item._step !== undefined`) |
| `packages/radix-ui/src/components/toc/default.tsx` | Cubic path smoothing with optional `thumbBox` lengths | Cubic bezier path calculation | Yes (`item._step !== undefined`) |
| `packages/radix-ui/src/components/toc/clerk.tsx` | Straight line segment transitions with polygon clipping | Straight line segment path calculation | Yes (`item._step !== undefined`) |

Sources: [packages/base-ui/src/components/toc/default.tsx:63-85](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/default.tsx#L63-L85), [packages/base-ui/src/components/toc/clerk.tsx:61-83](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/clerk.tsx#L61-L83), [packages/radix-ui/src/components/toc/default.tsx:63-85](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/toc/default.tsx#L63-L85), [packages/radix-ui/src/components/toc/clerk.tsx:61-83](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/toc/clerk.tsx#L61-L83)

> [!CAUTION]
> When `thumbBox` is enabled in the default variant, the component calculates path lengths using `path.getTotalLength()` and `path.getPointAtLength(l)`. If container heights resize to zero or headings have no corresponding DOM elements, `onPrint` returns early and sets the SVG state to `null`, disabling the indicator thumb entirely.

Sources: [packages/base-ui/src/components/toc/default.tsx:35-41](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/default.tsx#L35-L41), [packages/base-ui/src/components/toc/default.tsx:96-109](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/toc/default.tsx#L96-L109), [packages/radix-ui/src/components/toc/default.tsx:35-41](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/toc/default.tsx#L35-L41), [packages/radix-ui/src/components/toc/default.tsx:96-109](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/toc/default.tsx#L96-L109)

## Inline collapsible TOC components

### Component Implementation and Styling

The inline table of contents presentation is implemented through framework-specific UI components across packages, rendered as client components utilizing `@fuma-translate/react` translations and Lucide icons.

```tsx
'use client';

import { ChevronDown } from 'lucide-react';
import type { TOCItemType } from 'fumadocs-core/toc';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
import type { ComponentProps } from 'react';
import { cn } from '@/utils/cn';
import { useTranslations } from '@fuma-translate/react';

export interface InlineTocProps extends ComponentProps<typeof Collapsible> {
  items: TOCItemType[];
}
```

Sources: [packages/radix-ui/src/components/inline-toc.tsx:1-12](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/inline-toc.tsx#L1-L12), [packages/base-ui/src/components/inline-toc.tsx:1-12](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/inline-toc.tsx#L1-L12)

The `InlineTOC` component accepts an array of `TOCItemType` objects alongside standard component properties, wrapping content inside collapsible primitives with a customizable header trigger.

```tsx
export function InlineTOC({ items, ...props }: InlineTocProps) {
  const t = useTranslations({ note: 'inline table of contents' });

  return (
    <Collapsible
      {...props}
      className={cn(
        'not-prose rounded-lg border bg-fd-card text-fd-card-foreground',
        props.className,
      )}
    >
      <CollapsibleTrigger className="group inline-flex w-full items-center justify-between px-4 py-2.5 font-medium">
        {props.children ?? t('Table of Contents')}
        <ChevronDown className="size-4 transition-transform duration-200 group-data-[state=open]:rotate-180" />
      </CollapsibleTrigger>
      <CollapsibleContent>
        <div className="flex flex-col p-4 pt-0 text-sm text-fd-muted-foreground">
          {items.map((item) => (
            <a
              key={item.url}
              href={item.url}
              className="border-s py-1.5 hover:text-fd-accent-foreground"
              style={{
                paddingInlineStart: 12 * Math.max(item.depth - 1, 0),
              }}
            >
              {item.title}
            </a>
          ))}
        </div>
      </CollapsibleContent>
    </Collapsible>
  );
```

Sources: [packages/radix-ui/src/components/inline-toc.tsx:14-46](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/inline-toc.tsx#L14-L46), [packages/base-ui/src/components/inline-toc.tsx:14-48](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/inline-toc.tsx#L14-L48)

| Package Path | Class Name Handling | Trigger Chevron Animation | Translation Key |
| :--- | :--- | :--- | :--- |
| `packages/radix-ui/src/components/inline-toc.tsx` | `cn('not-prose rounded-lg border bg-fd-card text-fd-card-foreground', props.className)` | `group-data-[state=open]:rotate-180` | `Table of Contents` |
| `packages/base-ui/src/components/inline-toc.tsx` | `cn(..., typeof className === 'function' ? className(s) : className)` | `group-data-open:rotate-180` | `Table of Contents` |

Sources: [packages/radix-ui/src/components/inline-toc.tsx:20-27](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/inline-toc.tsx#L20-L27), [packages/base-ui/src/components/inline-toc.tsx:20-29](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/inline-toc.tsx#L20-L29)

> [!NOTE]
> Indentation for nested TOC items inside `InlineTOC` is calculated dynamically via inline styles using `12 * Math.max(item.depth - 1, 0)` for `paddingInlineStart`, coupled with a vertical border border-s on each anchor element.

Sources: [packages/radix-ui/src/components/inline-toc.tsx:31-41](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/inline-toc.tsx#L31-L41), [packages/base-ui/src/components/inline-toc.tsx:33-43](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/inline-toc.tsx#L33-L43)

## Layout slot integration and popovers

### Flux and Notebook TOC Slot Implementation

Documentation layouts integrate table of contents elements into specialized layout slots and collapsible popovers. In the Flux layout architecture, `TOC` mounts via a React portal targeting the DOM element with the ID `#flux-layout-slot`, wrapping its contents within `PageTOCPopover` and `PageTOCPopoverPhysical` to manage click-outside events and expansion states.

```tsx
function PageTOCPopover(props: ComponentProps<'div'>) {
  const [container, setContainer] = useState<HTMLElement | null>(null);

  useEffect(() => {
    const element = document.getElementById('flux-layout-slot');
    if (!element) return;
    setContainer(element);
  }, []);

  if (!container) return;
  return createPortal(<PageTOCPopoverPhysical {...props} />, container);
}
```

Sources: [packages/base-ui/src/layouts/flux/page/slots/toc.tsx:92-103](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/flux/page/slots/toc.tsx#L92-L103), [packages/radix-ui/src/layouts/flux/page/slots/toc.tsx:92-103](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/flux/page/slots/toc.tsx#L92-L103)

The notebook layout splits table of contents handling into a desktop sticky sidebar container (`#nd-toc`) and a collapsible mobile popover (`TOCPopover`). When no table of contents items, headers, or footers are present, the desktop layout returns a hidden placeholder div with fixed grid width configuration.

```tsx
export function TOC({ container, header, footer, style = 'normal', list }: TOCProps) {
  const t = useTranslations({ note: 'table of contents' });
  const items = Base.useTOCItems();
  const { TOCItems, TOCEmpty, TOCItem } = style === 'clerk' ? TocClerk : TocDefault;

  if (items.length === 0 && !footer && !header) {
    return <div id="nd-toc-placeholder" className="hidden xl:layout:[--fd-toc-width:268px]" />;
  }
```

Sources: [packages/base-ui/src/layouts/notebook/page/slots/toc.tsx:51-58](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/notebook/page/slots/toc.tsx#L51-L58), [packages/radix-ui/src/layouts/notebook/page/slots/toc.tsx:51-58](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/notebook/page/slots/toc.tsx#L51-L58)

### Popover Triggers and Progress Indicators

Layout popover triggers incorporate progress circles and animated text transitions using `motion.span` components. The progress calculation evaluates the active item index relative to total list items, ensuring smooth visual tracking of reading position.

```tsx
<ProgressCircle
  value={(items.findLastIndex((item) => item.active) + 1) / Math.max(1, items.length)}
  max={1}
  className={cn('shrink-0', open && 'text-fd-primary')}
/>
```

Sources: [packages/base-ui/src/layouts/flux/page/slots/toc.tsx:188-192](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/flux/page/slots/toc.tsx#L188-L192), [packages/radix-ui/src/layouts/flux/page/slots/toc.tsx:188-192](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/flux/page/slots/toc.tsx#L188-L192)

> [!WARNING]
> In `PageTOCPopoverPhysical`, window click listeners check whether target elements fall outside `ref.current`. If a portal-rendered trigger or external custom menu sits outside the popover header element, clicking it triggers an immediate collapse via `setOpen(false)`.

Sources: [packages/base-ui/src/layouts/flux/page/slots/toc.tsx:109-121](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/flux/page/slots/toc.tsx#L109-L121), [packages/radix-ui/src/layouts/flux/page/slots/toc.tsx:109-121](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/flux/page/slots/toc.tsx#L109-L121)

## Related

- [Shared Layout Architecture](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/shared-layout-architecture)


## Sitemap

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