diff --git a/demos/src/GuideNodeViews/TableOfContents/React/Component.jsx b/demos/src/GuideNodeViews/TableOfContents/React/Component.jsx
new file mode 100644
index 00000000..e5302385
--- /dev/null
+++ b/demos/src/GuideNodeViews/TableOfContents/React/Component.jsx
@@ -0,0 +1,63 @@
+import React, { useState, useEffect, useCallback } from 'react'
+import { NodeViewWrapper } from '@tiptap/react'
+import './Component.scss'
+
+export default ({ editor }) => {
+ const [items, setItems] = useState([])
+
+ const handleUpdate = useCallback(() => {
+ const headings = []
+ const transaction = editor.state.tr
+
+ editor.state.doc.descendants((node, pos) => {
+ if (node.type.name === 'heading') {
+ const id = `heading-${headings.length + 1}`
+
+ if (node.attrs.id !== id) {
+ transaction.setNodeMarkup(pos, undefined, {
+ ...node.attrs,
+ id,
+ })
+ }
+
+ headings.push({
+ level: node.attrs.level,
+ text: node.textContent,
+ id,
+ })
+ }
+ })
+
+ transaction.setMeta('preventUpdate', true)
+
+ editor.view.dispatch(transaction)
+
+ setItems(headings)
+ }, [editor])
+
+ useEffect(handleUpdate, [])
+
+ useEffect(() => {
+ if (!editor) {
+ return null
+ }
+
+ editor.on('update', handleUpdate)
+
+ return () => {
+ editor.off('update', handleUpdate)
+ }
+ }, [editor])
+
+ return (
+
+
+ {items.map((item, index) => (
+ -
+ {item.text}
+
+ ))}
+
+
+ )
+}
diff --git a/demos/src/GuideNodeViews/TableOfContents/React/Component.scss b/demos/src/GuideNodeViews/TableOfContents/React/Component.scss
new file mode 100644
index 00000000..f1629179
--- /dev/null
+++ b/demos/src/GuideNodeViews/TableOfContents/React/Component.scss
@@ -0,0 +1,43 @@
+.toc {
+ background: rgba(black, 0.1);
+ border-radius: 0.5rem;
+ opacity: 0.75;
+ padding: 0.75rem;
+
+ &__list {
+ list-style: none;
+ padding: 0;
+
+ &::before {
+ content: "Table of Contents";
+ display: block;
+ font-size: 0.75rem;
+ font-weight: 700;
+ letter-spacing: 0.025rem;
+ opacity: 0.5;
+ text-transform: uppercase;
+ }
+ }
+
+ &__item {
+ a:hover {
+ opacity: 0.5;
+ }
+
+ &--3 {
+ padding-left: 1rem;
+ }
+
+ &--4 {
+ padding-left: 2rem;
+ }
+
+ &--5 {
+ padding-left: 3rem;
+ }
+
+ &--6 {
+ padding-left: 4rem;
+ }
+ }
+}
diff --git a/demos/src/GuideNodeViews/TableOfContents/React/TableOfContents.js b/demos/src/GuideNodeViews/TableOfContents/React/TableOfContents.js
new file mode 100644
index 00000000..1e5e356c
--- /dev/null
+++ b/demos/src/GuideNodeViews/TableOfContents/React/TableOfContents.js
@@ -0,0 +1,40 @@
+import { Node, mergeAttributes } from '@tiptap/core'
+import { ReactNodeViewRenderer } from '@tiptap/react'
+import Component from './Component.jsx'
+
+export default Node.create({
+ name: 'tableOfContents',
+
+ group: 'block',
+
+ atom: true,
+
+ parseHTML() {
+ return [
+ {
+ tag: 'toc',
+ },
+ ]
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return ['toc', mergeAttributes(HTMLAttributes)]
+ },
+
+ addNodeView() {
+ return ReactNodeViewRenderer(Component)
+ },
+
+ addGlobalAttributes() {
+ return [
+ {
+ types: ['heading'],
+ attributes: {
+ id: {
+ default: null,
+ },
+ },
+ },
+ ]
+ },
+})
diff --git a/demos/src/GuideNodeViews/TableOfContents/React/index.html b/demos/src/GuideNodeViews/TableOfContents/React/index.html
new file mode 100644
index 00000000..7b996f50
--- /dev/null
+++ b/demos/src/GuideNodeViews/TableOfContents/React/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/src/GuideNodeViews/TableOfContents/React/index.jsx b/demos/src/GuideNodeViews/TableOfContents/React/index.jsx
new file mode 100644
index 00000000..3de6fbb9
--- /dev/null
+++ b/demos/src/GuideNodeViews/TableOfContents/React/index.jsx
@@ -0,0 +1,30 @@
+import React from 'react'
+import { useEditor, EditorContent } from '@tiptap/react'
+import StarterKit from '@tiptap/starter-kit'
+import TableOfContents from './TableOfContents.js'
+import './styles.scss'
+
+export default () => {
+ const editor = useEditor({
+ extensions: [StarterKit, TableOfContents],
+ content: `
+
+ 1 heading
+ paragraph
+ 1.1 heading
+ paragraph
+ 1.2 heading
+ paragraph
+ 2 heading
+ paragraph
+ 2.1 heading
+ paragraph
+ `,
+ })
+
+ if (!editor) {
+ return null
+ }
+
+ return
+}
diff --git a/demos/src/GuideNodeViews/TableOfContents/React/styles.scss b/demos/src/GuideNodeViews/TableOfContents/React/styles.scss
new file mode 100644
index 00000000..46b51a4e
--- /dev/null
+++ b/demos/src/GuideNodeViews/TableOfContents/React/styles.scss
@@ -0,0 +1,6 @@
+/* Basic editor styles */
+.ProseMirror {
+ > * + * {
+ margin-top: 0.75em;
+ }
+}