Table of Contents extension
The TableOfContents extension lets you get a list of anchors from your document and passes on important information about each anchor (for example the depth, the content and a unique ID for each heading but also the active state and scroll states for each anchor). This can be used to render the table of content on your own.
Install
Once done, you can install the extension from our private registry:
npm install @tiptap/extension-table-of-contentsSettings
anchorTypes
The types of the nodes you want to use for your Table of Content. By default this is ["heading"] but in case you create your own custom Heading extension OR extend the existing one and use a different name, you can pass that name here.
Default: ["heading"]
TableOfContents.configure({
anchorTypes: ['heading', 'customAnchorType'],
})getIndex
This option can be used to customize how the item indexes are calculated. By default this is using an internal function but it can be overwritten to do some custom logic.
TableOfContents.configure({
getIndex: (anchor, previousAnchors, level) => {
// do some custom logic, but for this example we will just return 1
return 1
},
})We expose two ready to use functions - one to generate linear indexes which continue to count from 1 to n and one to generate hierarchical indexes that will count from 1 to n for each level.
import { getLinearIndexes, getHierarchicalIndexes } from '@tiptap/extension-table-of-contents'
// generate linear indexes
TableOfContents.configure({
getIndex: getLinearIndexes,
})
// generate hierarchical indexes
TableOfContents.configure({
getIndex: getHierarchicalIndexes,
})getLevel
This option can be used to customize how item levels are generated. By default the normal level generation is used that checks for heading element level attributes. If you want to customize this because for example you want to include custom anchors in your heading generation, you can use this to do so.
TableOfContents.configure({
getLevel: (anchor, previousAnchors) => {
// do some custom logic, but for this example we will just return 1
return 1
},
})getId
A builder function that returns a unique ID for each heading. Inside the argument you get access to the headings text content (for example you want to generate IDs based on the text content of the heading).
By default this is a function that uses the uuid package to generate a unique ID.
Default: () => uuid()
// here we use an imaginary "slugify" function
// you should probably also add a unique identifier to the slug
TableOfContents.configure({
getId: (content) => slugify(content),
})scrollParent
The scroll parent you want to attach to. This is used to determine which heading currently is active or was already scrolled over. By default this is a callback function that returns the window but you can pass a callback that returns any HTML element here.
Default: () => window
// For example the editors DOM element itself is the scrolling element
TableOfContents.configure({
scrollParent: () => editor.view.dom,
})onUpdate
The most important option that you must set to use this extension. This is a callback function that gets called whenever the Table of Content updates. You get access to an array of heading data (see below) which you can use to render your own Table of Content.
To render the table of content you can render it by any means you want. You can use a framework like Vue, React or Svelte or you can use a simple templating engine like Handlebars or Pug. You can also use a simple document.createElement to render the table of content.
You can pass a second argument to get the information whether this is the initial creation step for the ToC data.
Default: undefined
// with vanilla JS
const tocElement = document.createElement('div')
document.body.appendChild(tocElement)
TableOfContents.configure({
onUpdate: (anchors, isCreate) => {
tocElement.innerHTML = ''
if (isCreate) {
console.log('This is the inital creation step for the ToC data')
}
anchors.forEach((anchor) => {
const anchorElement = document.createElement('div')
anchorElement.innerHTML = anchor.content
anchorElement.dataset.id = anchor.id
anchorElement.dataset.depth = anchor.depth
anchorElement.dataset.active = anchor.active
anchorElement.dataset.scrolled = anchor.scrolled
tocElement.appendChild(anchorElement)
})
},
})// with react
const [anchors, setAnchors] = useState([])
// inside the useEditor hook you could then do something like that:
TableOfContents.configure({
onUpdate: (anchors) => {
setAnchors(anchors)
},
})// with vue
const anchors = ref([])
TableOfContents.configure({
onUpdate: (anchors) => {
anchors.value = anchors
},
})Storage
content
The heading content of the current document
editor.storage.tableOfContents.contentanchors
An array of HTML nodes
editor.storage.tableOfContents.anchorsscrollHandler
The scrollHandler used by the scroll function. Should not be changed or edited but could be used to manually bind this function somewhere else
editor.storage.tableOfContents.scrollHandler()scrollPosition
The current scrollPosition inside the scrollParent.
editor.storage.tableOfContents.scrollPositionThe anchors array
The array returned by the storage or the onUpdate function includes objects structured like this:
{
dom: HTMLElement // the HTML element for this anchor
editor: Editor // the editor
id: string // the node id
isActive: boolean // whether this anchor is currently active
isScrolledOver: boolean // whether this anchor was already scrolled over
itemIndex: number // the index of the item on its current level
level: number // the current level of the item - this could be different from the actual anchor level and is used to render the hierarchy from high to low headlines
node: Node // the ProseMirror node for this anchor
originalLevel: number // the actual level
pos: number // the position of the anchor node
textContent: string // the text content of the anchor
}This should give you enough flexibility to render your own table of content.
Server side anchor ID utility
The generateTocIds function allows you to assign anchor IDs (id and data-toc-id) to a Tiptap document on the server side, without needing to create an Editor instance. This is useful when you want to render the document statically — for example with @tiptap/static-renderer — and still get the same anchor attributes the TableOfContents plugin would produce in a live editor.
Parameters
doc(JSONContent): A Tiptap JSON document to add anchor IDs toextensions(Extensions): The extensions to use. Must include theTableOfContentsextension. To customize how the IDs are generated or which node types are treated as anchors, pass options likegetIdoranchorTypesto theTableOfContentsextension.
Return type
Returns a new Tiptap document (a JSONContent object) with id and data-toc-id attributes assigned to anchor-type nodes that were missing or had duplicate IDs.
Example
import { generateTocIds, TableOfContents } from '@tiptap/extension-table-of-contents'
import { StarterKit } from '@tiptap/starter-kit'
const doc = {
type: 'doc',
content: [
{ type: 'heading', attrs: { level: 1 }, content: [{ type: 'text', text: 'Introduction' }] },
{ type: 'paragraph', content: [{ type: 'text', text: 'A paragraph.' }] },
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Details' }] },
],
}
const newDoc = generateTocIds(doc, [StarterKit, TableOfContents])
// Result:
// {
// type: 'doc',
// content: [
// { type: 'heading', attrs: { level: 1, id: 'd4590f81-52e8-45ec-b317-2e9a805b03e3', 'data-toc-id': 'd4590f81-52e8-45ec-b317-2e9a805b03e3' }, content: [...] },
// { type: 'paragraph', content: [...] },
// { type: 'heading', attrs: { level: 2, id: 'c88f9b5f-7b91-442f-b4d9-ee0d04104827', 'data-toc-id': 'c88f9b5f-7b91-442f-b4d9-ee0d04104827' }, content: [...] }
// ]
// }The function automatically picks up the configuration from the TableOfContents extension, including anchorTypes and getId.