Table of Contents Node
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.
Installation
Add the component via the Tiptap CLI:
npx @tiptap/cli@latest add toc-nodeUsage
Basic Integration
To add table of contents functionality to your Tiptap editor, follow these steps:
1. Import the required extensions and components:
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:
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:
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 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:
// Insert programmatically
editor.commands.insertTocNode({
maxShowCount: 10,
topOffset: 60,
showTitle: true,
})2. Floating TOC Sidebar
A sidebar component that floats alongside your editor:
<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
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:
// 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:
- Manual clicks (highest priority)
- Scroll position tracking
- Cursor/selection position
- 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
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:
- Manual Click: When a user clicks a TOC item, that item remains active
- Scroll Detection: After manual navigation completes, switches to scroll-based detection
- Cursor Position: Highlights heading where the cursor is currently positioned
- 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
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:
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 shownisActive: boolean- Whether the title is currently visiblecanToggle: boolean- Whether the toggle action can be performedhandleToggle: () => boolean- Execute the toggle actionlabel: string- UI label for the buttonIcon: 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
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
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.
navigateToHeading(item, {
topOffset: 80, // Offset from top (pixels)
behavior: 'smooth', // 'smooth' | 'auto'
})Parameters:
item: TableOfContentDataItem- The heading item to navigate tooptions.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.
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 h3Algorithm:
- Rebases all levels so the minimum level becomes 1 (root level)
- For each heading, finds the most recent previous heading with a smaller level
- If found, nests it under that parent (parent depth + 1)
- If not found, treats it as a root-level item (depth = 1)
Complete Example
Here's a comprehensive example integrating all components:
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
import '@/components/tiptap-node/toc-node/toc-node.scss'
import '@/components/tiptap-node/toc-node/ui/toc-sidebar/toc-sidebar.scss'TocNode CSS Classes
.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
.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:
.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:
: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)reactandreact-dom- React framework
Optional Packages
@tiptap/extension-unique-id- Automatic heading ID generationsass/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