Creating an editor with Tracked Changes

Paid add-on

Learn how to create a fully functional editor with tracked changes, including a toolbar to toggle tracking and accept or reject suggestions.

Framework note

This guide uses React, but the same concepts apply to Vue and any other framework supported by Tiptap.

Prerequisites

Make sure you have the Tracked Changes extension installed. See the installation guide for details.

Set up the editor

Start by creating an editor instance with the TrackedChanges extension. You'll want to pass in the current user's ID and metadata so suggestions are attributed correctly.

import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { TrackedChanges } from '@tiptap-pro/extension-tracked-changes'

function Editor() {
  const editor = useEditor({
    extensions: [
      StarterKit,
      TrackedChanges.configure({
        enabled: true,
        userId: 'user-123',
        userMetadata: {
          name: 'John Doe',
        },
      }),
    ],
    content: '<p>Start editing to see tracked changes in action.</p>',
  })

  if (!editor) {
    return null
  }

  return <EditorContent editor={editor} />
}

At this point, every edit the user makes is tracked as a suggestion. Insertions appear as add suggestions and deletions as delete suggestions.

Add a toolbar

Next, add controls to toggle tracking and accept or reject suggestions. The extension provides editor commands for all of these operations.

import { useCallback, useState } from 'react'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { TrackedChanges } from '@tiptap-pro/extension-tracked-changes'

function Editor() {
  const [isEnabled, setIsEnabled] = useState(true)

  const editor = useEditor({
    extensions: [
      StarterKit,
      TrackedChanges.configure({
        enabled: true,
        userId: 'user-123',
        userMetadata: { name: 'John Doe' },
      }),
    ],
    content: '<p>Start editing to see tracked changes in action.</p>',
  })

  const toggleTrackedChanges = useCallback(() => {
    if (!editor) return
    editor.commands.toggleTrackedChanges()
    setIsEnabled(!isEnabled)
  }, [editor, isEnabled])

  if (!editor) {
    return null
  }

  return (
    <div>
      <div className="toolbar">
        <button onClick={toggleTrackedChanges}>
          {isEnabled ? 'Disable' : 'Enable'} Track Changes
        </button>
        <button onClick={() => editor.commands.acceptAllSuggestions()}>
          Accept All
        </button>
        <button onClick={() => editor.commands.rejectAllSuggestions()}>
          Reject All
        </button>
      </div>
      <EditorContent editor={editor} />
    </div>
  )
}

Add suggestion styles

Suggestions render as <span> elements with data attributes. Add CSS to visually distinguish insertions from deletions.

/* Insertions — green highlight */
[data-suggestion-type="add"] {
  background-color: rgba(0, 255, 0, 0.15);
  text-decoration: underline;
}

/* Deletions — red strikethrough */
[data-suggestion-type="delete"] {
  background-color: rgba(255, 0, 0, 0.15);
  text-decoration: line-through;
}

/* Replacement deletions */
[data-suggestion-type="replaceDeletion"] {
  background-color: rgba(255, 0, 0, 0.15);
  text-decoration: line-through;
}

/* Replacement insertions */
[data-suggestion-type="replaceInsertion"] {
  background-color: rgba(0, 255, 0, 0.15);
  text-decoration: underline;
}

See the Styling reference for all available data attributes.

Accept and reject at the cursor

You can also accept or reject the suggestion at the current cursor position without passing an ID:

<button onClick={() => editor.commands.acceptSuggestion()}>
  Accept at cursor
</button>
<button onClick={() => editor.commands.rejectSuggestion()}>
  Reject at cursor
</button>

Accept and reject in a selection

To accept or reject all suggestions within the current text selection:

const acceptInSelection = useCallback(() => {
  if (!editor) return
  const { from, to } = editor.state.selection
  editor.commands.acceptSuggestionsInRange({ from, to })
}, [editor])

const rejectInSelection = useCallback(() => {
  if (!editor) return
  const { from, to } = editor.state.selection
  editor.commands.rejectSuggestionsInRange({ from, to })
}, [editor])

Switch users

In a multi-user application, you'll switch the tracked changes user when the active user changes. Use the setTrackedChangesUser command:

editor.commands.setTrackedChangesUser({
  userId: 'user-456',
  userMetadata: { name: 'Jane Smith' },
})

Enable code block tracking

By default, code blocks don't support marks. The extension provides a helper to patch any code block extension for tracked changes support:

import CodeBlock from '@tiptap/extension-code-block'
import { TrackedChanges, withSuggestionMarkOnCodeBlock } from '@tiptap-pro/extension-tracked-changes'

const TrackableCodeBlock = withSuggestionMarkOnCodeBlock(CodeBlock)

const editor = useEditor({
  extensions: [
    StarterKit.configure({ codeBlock: false }),
    TrackableCodeBlock,
    TrackedChanges.configure({ enabled: true, userId: 'user-123' }),
  ],
})

Next steps