---
title: "Table of Contents Node"
description: "Add a table of contents node UI component to your Tiptap editor. More in the documentation!"
canonical_url: "https://tiptap.dev/docs/ui-components/node-components/table-of-contents-node"
---

# Table of Contents Node

Add a table of contents node UI component to your Tiptap editor. More in the documentation!

A comprehensive table of contents (TOC) component system for Tiptap editors. This package provides both an inline TOC node that can be embedded within the editor content and a floating sidebar component for document navigation with progress indicators.

> **Interactive demo:** [toc node](https://template.tiptap.dev/preview/tiptap-node/toc-node)

## Installation

Add the component via the Tiptap CLI:

```bash
npx @tiptap/cli@latest add toc-node
```

## Usage

### Basic Integration

To add table of contents functionality to your Tiptap editor, follow these steps:

**1. Import the required extensions and components:**

```tsx
import { Heading } from '@tiptap/extension-heading'
import { TableOfContents } from '@tiptap/extension-table-of-contents'
import { TocNode, TocSidebar, TocShowTitleButton } from '@/components/tiptap-node/toc-node'
import { TocProvider, useToc } from '@/components/tiptap-node/toc-node/context/toc-context'

// Import required styles
import '@/components/tiptap-node/toc-node/toc-node.scss'
import '@/components/tiptap-node/toc-node/ui/toc-sidebar/toc-sidebar.scss'
```

**2. Add extensions to your editor configuration:**

```tsx
function EditorComponent() {
  const { setTocContent } = useToc()

  const editor = useEditor({
    extensions: [
      StarterKit,
      Heading.configure({
        levels: [1, 2, 3, 4, 5, 6],
      }),
      TableOfContents.configure({
        getIndex: (headingNode) => headingNode.attrs.id,
        onUpdate: (content) => {
          setTocContent(content) // Update TOC state
        },
      }),
      TocNode.configure({
        topOffset: 80,
        maxShowCount: 20,
        showTitle: true,
      }),
    ],
    content: '<p>Your content here</p>',
  })
}
```

**3. Add the TOC components to your editor:**

```tsx
import { TocProvider } from '@/components/tiptap-node/toc-node/context/toc-context'
import { TocSidebar } from '@/components/tiptap-node/toc-node'
;<TocProvider>
  <EditorContext.Provider value={{ editor }}>
    {/* Toolbar with TOC button */}
    <div className="toolbar">
      <button onClick={() => editor?.commands.insertTocNode()}>Insert TOC</button>
      <TocShowTitleButton editor={editor} />
    </div>

    <div className="editor-layout">
      <EditorContent editor={editor} />

      {/* Floating sidebar */}
      <TocSidebar maxShowCount={20} topOffset={80} />
    </div>
  </EditorContext.Provider>
</TocProvider>
```

**That's it!** Your editor now has:

- Inline TOC nodes that can be inserted into documents
- Floating TOC sidebar with progress indicators
- Automatic heading detection and navigation
- URL hash support for deep linking

For a complete example with all features, see the [Complete Example](#complete-example) section below.

## Creating Table of Contents

### Inserting TOC Nodes

The TOC system provides two primary ways to display a table of contents:

#### 1. Inline TOC Node

An inline node that can be placed anywhere in your document content:

```tsx
// Insert programmatically
editor.commands.insertTocNode({
  maxShowCount: 10,
  topOffset: 60,
  showTitle: true,
})
```

#### 2. Floating TOC Sidebar

A sidebar component that floats alongside your editor:

```tsx
<TocSidebar maxShowCount={20} topOffset={80} className="my-toc-sidebar" />
```

## Components

### `<TocNode />` Extension

**An inline table of contents node that can be embedded directly in your document.** This extension creates a draggable, selectable block that automatically updates as headings change.

#### Key Features

- **Auto-updating**: Automatically reflects heading changes in the document
- **Customizable Display**: Control max headings, title visibility, and scroll offset
- **Smart Navigation**: Click to scroll to headings with smooth scrolling
- **Empty State**: Shows helpful message when no headings exist
- **Depth Normalization**: Proper hierarchical nesting of headings

#### Usage

```tsx
import { TocNode } from '@/components/tiptap-node/toc-node'
import { TableOfContents } from '@tiptap/extension-table-of-contents'

const editor = useEditor({
  extensions: [
    TableOfContents.configure({
      getIndex: (headingNode) => headingNode.attrs.id,
      onUpdate: (content) => {
        // Handle updates
      },
    }),
    TocNode.configure({
      topOffset: 80,
      maxShowCount: 20,
      showTitle: true,
      HTMLAttributes: {
        class: 'my-custom-toc',
      },
    }),
  ],
})

// Insert the node
editor.commands.insertTocNode({
  maxShowCount: 15,
  topOffset: 60,
})
```

#### Configuration Options

| Option           | Type                  | Default | Description                                                                 |
| ---------------- | --------------------- | ------- | --------------------------------------------------------------------------- |
| `topOffset`      | `number`              | `0`     | Offset from the top of the viewport when scrolling to a heading (in pixels) |
| `maxShowCount`   | `number`              | `20`    | Maximum number of headings to display in the TOC                            |
| `showTitle`      | `boolean`             | `true`  | Whether to show the "Table of contents" title                               |
| `HTMLAttributes` | `Record<string, any>` | `{}`    | HTML attributes to add to the TOC node element                              |

#### Commands

The extension provides the `insertTocNode` command:

```tsx
// Insert with default settings
editor.commands.insertTocNode()

// Insert with custom attributes
editor.commands.insertTocNode({
  maxShowCount: 10,
  topOffset: 100,
  showTitle: false,
})
```

### `<TocSidebar />`

**A floating sidebar component that displays the table of contents with visual progress indicators.** This component provides an always-visible navigation panel with advanced state management and smooth scrolling.

#### Key Features

- **Progress Rail**: Visual progress indicator showing document position
- **Multi-level Active State**:
  1. Manual clicks (highest priority)
  2. Scroll position tracking
  3. Cursor/selection position
  4. First heading (fallback)
- **Smart Scrolling**: Only scrolls when target heading is not visible
- **URL Hash Support**: Restores scroll position from URL on page load
- **Smooth Transitions**: Animated active state changes

#### Usage

```tsx
import { TocProvider } from '@/components/tiptap-node/toc-node/context/toc-context'
import { TocSidebar } from '@/components/tiptap-node/toc-node'

function MyEditor() {
  return (
    <TocProvider>
      <div className="editor-container">
        <EditorContent editor={editor} />
        <TocSidebar maxShowCount={20} topOffset={80} className="custom-sidebar" />
      </div>
    </TocProvider>
  )
}
```

#### Props

| Prop                        | Type     | Default | Description                                             |
| --------------------------- | -------- | ------- | ------------------------------------------------------- |
| `maxShowCount`              | `number` | `20`    | Maximum number of headings to show in the TOC           |
| `topOffset`                 | `number` | `0`     | Offset from the top of the editor container (in pixels) |
| `className`                 | `string` | -       | Additional CSS class names                              |
| All standard HTML div props | -        | -       | Supports all `HTMLAttributes<HTMLDivElement>`           |

#### Active State Logic

The sidebar intelligently determines which heading to highlight:

1. **Manual Click**: When a user clicks a TOC item, that item remains active
2. **Scroll Detection**: After manual navigation completes, switches to scroll-based detection
3. **Cursor Position**: Highlights heading where the cursor is currently positioned
4. **Fallback**: Shows first heading when no other criteria are met

### `<TocShowTitleButton />`

**A toolbar button for toggling the visibility of the TOC node title.** This button only appears when a TOC node is selected in the editor.

#### Usage

```tsx
import { TocShowTitleButton } from '@/components/tiptap-node/toc-node'

function MyToolbar() {
  return (
    <div className="toolbar">
      <TocShowTitleButton
        editor={editor}
        text="Show Title"
        hideWhenUnavailable={true}
        onToggle={(isActive) => {
          console.log('Title visibility:', isActive)
        }}
      />
    </div>
  )
}
```

#### Props

| Prop                      | Type                        | Default | Description                                  |
| ------------------------- | --------------------------- | ------- | -------------------------------------------- |
| `editor`                  | `Editor \| null`            | -       | The TipTap editor instance                   |
| `text`                    | `string`                    | -       | Optional text to display alongside the icon  |
| `hideWhenUnavailable`     | `boolean`                   | `false` | Hide the button when no TOC node is selected |
| `onToggle`                | `(active: boolean) => void` | -       | Callback fired when the toggle state changes |
| All standard button props | -                           | -       | Supports all `ButtonProps`                   |

#### Custom Implementation with Hook

For custom button implementations, use the `useTocShowTitle` hook:

```tsx
import { useTocShowTitle } from '@/components/tiptap-node/toc-node'

function CustomShowTitleButton() {
  const { isVisible, isActive, canToggle, handleToggle, label, Icon } = useTocShowTitle({
    editor,
    hideWhenUnavailable: true,
    onToggle: (isActive) => {
      console.log('Title visibility:', isActive)
    },
  })

  if (!isVisible) return null

  return (
    <button onClick={handleToggle} disabled={!canToggle} aria-label={label} data-active={isActive}>
      <Icon /> {label}
    </button>
  )
}
```

The hook returns:

- `isVisible: boolean` - Whether the button should be shown
- `isActive: boolean` - Whether the title is currently visible
- `canToggle: boolean` - Whether the toggle action can be performed
- `handleToggle: () => boolean` - Execute the toggle action
- `label: string` - UI label for the button
- `Icon: ComponentType` - Icon component for the button

## Context & Hooks

### `<TocProvider />`

A context provider that manages TOC state and provides navigation methods to all child components.

#### Usage

```tsx
import { TocProvider } from '@/components/tiptap-node/toc-node/context/toc-context'

function App() {
  return (
    <TocProvider>
      <YourEditorComponents />
    </TocProvider>
  )
}
```

### `useToc()`

Access TOC state and methods from any component within the provider.

#### API

```tsx
const {
  tocContent, // TableOfContentData | null
  setTocContent, // (value: TableOfContentData | null) => void
  navigateToHeading, // (item, options?) => void
  normalizeHeadingDepths, // <T>(items: T[]) => number[]
} = useToc()
```

#### Methods

**navigateToHeading(item, options?)**

Scrolls to a heading and updates the editor selection.

```tsx
navigateToHeading(item, {
  topOffset: 80, // Offset from top (pixels)
  behavior: 'smooth', // 'smooth' | 'auto'
})
```

Parameters:

- `item: TableOfContentDataItem` - The heading item to navigate to
- `options.topOffset?: number` - Offset from viewport top (default: 0)
- `options.behavior?: ScrollBehavior` - Scroll behavior (default: 'smooth')

**normalizeHeadingDepths(items)**

Normalizes heading depths for proper hierarchical nesting. This ensures headings can only be nested under previous headings with smaller level numbers, preventing incorrect structures.

```tsx
const items = [
  { level: 2, textContent: 'Section 1' },
  { level: 4, textContent: 'Subsection' }, // h4 can't be child of h2
  { level: 3, textContent: 'Another Section' },
]

const depths = normalizeHeadingDepths(items)
// Returns: [1, 2, 2]
// The h4 is adjusted to be at the same depth as a properly nested h3
```

Algorithm:

1. Rebases all levels so the minimum level becomes 1 (root level)
2. For each heading, finds the most recent previous heading with a smaller level
3. If found, nests it under that parent (parent depth + 1)
4. If not found, treats it as a root-level item (depth = 1)

## Complete Example

Here's a comprehensive example integrating all components:

```tsx
import { useEditor, EditorContent, EditorContext } from '@tiptap/react'
import { StarterKit } from '@tiptap/starter-kit'
import { Heading } from '@tiptap/extension-heading'
import { TableOfContents } from '@tiptap/extension-table-of-contents'
import { TocProvider, useToc } from '@/components/tiptap-node/toc-node/context/toc-context'
import { TocNode, TocSidebar, TocShowTitleButton } from '@/components/tiptap-node/toc-node'

import '@/components/tiptap-node/toc-node/toc-node.scss'
import '@/components/tiptap-node/toc-node/ui/toc-sidebar/toc-sidebar.scss'

function EditorComponent() {
  const { setTocContent } = useToc()

  const editor = useEditor({
    extensions: [
      StarterKit,
      Heading.configure({
        levels: [1, 2, 3, 4, 5, 6],
      }),
      TableOfContents.configure({
        getIndex: (headingNode) => headingNode.attrs.id,
        onUpdate: (content) => {
          setTocContent(content)
        },
      }),
      TocNode.configure({
        topOffset: 80,
        maxShowCount: 20,
        showTitle: true,
      }),
    ],
    content: `
      <h1 id="introduction">Introduction</h1>
      <p>Welcome to this comprehensive document about table of contents...</p>
      <h2 id="getting-started">Getting Started</h2>
      <p>Let's begin by installing the required packages...</p>
      <h3 id="installation">Installation</h3>
      <p>Run the following command to install...</p>
      <h3 id="configuration">Configuration</h3>
      <p>Configure your editor with these options...</p>
      <h2 id="usage">Usage</h2>
      <p>Here's how to use the TOC components...</p>
      <h3 id="inline-toc">Inline TOC Node</h3>
      <p>Insert a TOC node directly in your document...</p>
      <h3 id="sidebar-toc">Sidebar TOC</h3>
      <p>Add a floating sidebar for navigation...</p>
      <h2 id="advanced">Advanced Features</h2>
      <p>Explore advanced functionality...</p>
    `,
  })

  return (
    <EditorContext.Provider value={{ editor }}>
      <div className="editor-wrapper">
        {/* Toolbar */}
        <div className="toolbar">
          <button
            onClick={() => editor?.commands.insertTocNode()}
            disabled={!editor?.can().insertTocNode()}
          >
            Insert TOC
          </button>
          <TocShowTitleButton editor={editor} text="Toggle Title" hideWhenUnavailable={true} />
        </div>

        {/* Editor with Sidebar */}
        <div className="editor-content-wrapper">
          <EditorContent editor={editor} className="editor-content" />
          <TocSidebar maxShowCount={20} topOffset={80} className="editor-toc-sidebar" />
        </div>
      </div>
    </EditorContext.Provider>
  )
}

export default function App() {
  return (
    <TocProvider>
      <EditorComponent />
    </TocProvider>
  )
}
```

## Styling

The TOC components include default styles that can be customized using CSS or SCSS.

### Required Stylesheets

```tsx
import '@/components/tiptap-node/toc-node/toc-node.scss'
import '@/components/tiptap-node/toc-node/ui/toc-sidebar/toc-sidebar.scss'
```

### TocNode CSS Classes

```scss
.tiptap-table-of-contents-node {
  // Main container for the TOC node
  padding: 1rem;
  border: 1px solid var(--border-color);
  border-radius: 0.5rem;
}

.tiptap-table-of-contents-title {
  // Title heading "Table of contents"
  font-size: 1.25rem;
  font-weight: 600;
  margin-bottom: 0.5rem;
}

.tiptap-table-of-contents-list {
  // Navigation list container
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.tiptap-table-of-contents-item {
  // Individual TOC items
  padding: 0.5rem;
  border-radius: 0.25rem;
  text-decoration: none;
  color: var(--text-color);
  transition: background-color 0.2s;

  &:hover {
    background-color: var(--hover-bg);
  }

  // Depth-based indentation
  &[data-depth='1'] {
    padding-left: 0.5rem;
  }
  &[data-depth='2'] {
    padding-left: 1.5rem;
  }
  &[data-depth='3'] {
    padding-left: 2.5rem;
  }
}

.tiptap-table-of-contents-empty {
  // Empty state message
  color: var(--text-muted);
  font-style: italic;
}
```

### TocSidebar CSS Classes

```scss
.toc-sidebar {
  // Main sidebar container
  position: sticky;
  top: 80px;
  max-height: calc(100vh - 100px);
  overflow-y: auto;
}

.toc-sidebar-wrapper {
  // Inner wrapper
  display: flex;
  gap: 0.75rem;
}

.toc-sidebar-progress {
  // Progress rail container
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  width: 3px;
}

.toc-sidebar-progress-line {
  // Individual progress lines
  height: 1.5rem;
  background-color: var(--progress-inactive);
  transition: background-color 0.2s;

  &--active {
    background-color: var(--progress-active);
  }
}

.toc-sidebar-nav {
  // Navigation container
  flex: 1;
}

.toc-sidebar-item {
  // Sidebar navigation items
  display: block;
  padding: 0.375rem 0.5rem;
  border-radius: 0.25rem;
  text-decoration: none;
  color: var(--text-color);
  font-size: 0.875rem;
  line-height: 1.5;
  transition: all 0.2s;

  &:hover {
    background-color: var(--hover-bg);
  }

  &--active {
    font-weight: 600;
    color: var(--active-color);
  }
}
```

### Custom Depth Styling

Both components expose a `--toc-depth` CSS variable for custom indentation:

```scss
.tiptap-table-of-contents-item {
  // Use CSS variable for dynamic indentation
  padding-left: calc(var(--toc-depth, 1) * 1rem);
}

.toc-sidebar-item {
  // Adjust for sidebar (depth 1 = no indent)
  padding-left: calc((var(--toc-depth, 1) - 1) * 0.75rem);

  // Optional: Add visual indicator for depth
  &::before {
    content: '';
    display: inline-block;
    width: calc(var(--toc-depth, 1) * 4px);
    height: 2px;
    background: currentColor;
    margin-right: 0.5rem;
    opacity: 0.3;
  }
}
```

### Dark Mode Support

Add dark mode styles using CSS variables or class-based theming:

```scss
:root {
  --toc-bg: #ffffff;
  --toc-text: #1a1a1a;
  --toc-border: #e5e5e5;
  --toc-hover: #f5f5f5;
  --toc-active: #0066ff;
  --toc-progress-inactive: #e5e5e5;
  --toc-progress-active: #0066ff;
}

.dark {
  --toc-bg: #1a1a1a;
  --toc-text: #ffffff;
  --toc-border: #333333;
  --toc-hover: #2a2a2a;
  --toc-active: #4d9aff;
  --toc-progress-inactive: #333333;
  --toc-progress-active: #4d9aff;
}
```

## Dependencies

### Required Packages

- `@tiptap/core` - Core TipTap functionality
- `@tiptap/react` - React integration for TipTap
- `@tiptap/extension-table-of-contents` - TOC content extraction
- `@tiptap/extension-heading` - Heading nodes (for TOC sources)
- `react` and `react-dom` - React framework

### Optional Packages

- `@tiptap/extension-unique-id` - Automatic heading ID generation
- `sass` / `sass-embedded` - For SCSS compilation (if using SCSS)

## Features

- ✅ **Inline TOC Node**: Embed table of contents directly in documents
- ✅ **Floating Sidebar**: Always-visible navigation panel
- ✅ **Auto-updating**: Real-time updates as headings change
- ✅ **Smart Navigation**: Intelligent scroll and active state management
- ✅ **Progress Indicators**: Visual progress rail showing document position
- ✅ **Multi-level Detection**: Click, scroll, and cursor-based active states
- ✅ **Depth Normalization**: Proper hierarchical heading structure
- ✅ **URL Hash Support**: Restore scroll position from URL
- ✅ **Empty State**: Helpful message when no headings exist
- ✅ **Smooth Scrolling**: Animated scroll-to-heading transitions
- ✅ **Configurable Display**: Control max items, title visibility, scroll offset
- ✅ **Keyboard Accessible**: Full keyboard navigation support
- ✅ **Dark Mode**: Built-in dark mode theming
- ✅ **TypeScript**: Comprehensive type definitions
- ✅ **Customizable Styling**: CSS variables and class-based theming

## Related Extensions

- **Heading Extension** - Required for creating heading nodes
- **UniqueID Extension** - Recommended for stable heading IDs
- **Table of Contents Extension** - Required for content extraction
