Split view

Build a side-by-side comparison that shows the original and AI-modified versions of a document next to each other. Each change block is highlighted and can be individually accepted or rejected. Both panels stay vertically aligned via automatically computed spacers.

How it works

The split view uses three editors: a main editor (holds the document and tracked changes, typically hidden during split view), a left editor (shows the original), and a right editor (shows the modified version). Two separate panel editors are required because each loads a different document snapshot with different decorations.

createSplitView() wires all three together. It derives the before/after snapshots from the main editor's tracked changes, loads them into the panel editors, renders highlights, and keeps both panels vertically aligned with spacers. Scroll sync and hover highlighting are handled automatically when scroll containers are provided.

Set up the editors

The main editor needs AiToolkit and TrackedChanges. The panel editors only need AiToolkit and are read-only.

import { useEditor } from '@tiptap/react'
import { AiToolkit } from '@tiptap-pro/ai-toolkit'
import { TrackedChanges } from '@tiptap-pro/extension-tracked-changes'

const mainEditor = useEditor({
  extensions: [
    // ... your content extensions
    AiToolkit.configure({ /* ... */ }),
    TrackedChanges,
  ],
})

const leftEditor = useEditor({
  editable: false,
  extensions: [
    // ... same content extensions (without TrackedChanges)
    AiToolkit.configure({ /* ... */ }),
  ],
})

const rightEditor = useEditor({
  editable: false,
  extensions: [
    // ... same content extensions (without TrackedChanges)
    AiToolkit.configure({ /* ... */ }),
  ],
})

Create the split view

import { getAiToolkit } from '@tiptap-pro/ai-toolkit'

const splitView = getAiToolkit(mainEditor).createSplitView({
  leftEditor,
  rightEditor,
  leftContainer: leftScrollRef.current,   // optional: enables scroll sync
  rightContainer: rightScrollRef.current, // optional: enables scroll sync
})

// Tear down when the component unmounts
return () => splitView.destroy()

Accept and reject changes

// Accept or reject a single entry
splitView.acceptEntry('diff-tc-abc123')
splitView.rejectEntry('diff-tc-abc123')

// Accept or reject all at once
splitView.acceptAll()
splitView.rejectAll()

To react when the list of entries changes:

splitView.on('sync', (entries) => {
  setDiffEntries(entries)

  if (entries.length === 0) {
    closeSplitView()
  }
})

CSS

.split-diff-deletion {
  background-color: rgba(161, 161, 170, 0.15);
  color: #71717a;
  text-decoration: line-through;
  text-decoration-color: rgba(161, 161, 170, 0.5);
  cursor: pointer;
}

.split-diff-insertion {
  background-color: rgba(34, 197, 94, 0.2);
  color: #16a34a;
  cursor: pointer;
}

.split-diff-deletion.split-diff-highlight {
  background-color: rgba(161, 161, 170, 0.3);
}

.split-diff-insertion.split-diff-highlight {
  background-color: rgba(34, 197, 94, 0.35);
}

.split-diff-spacer {
  background-color: rgba(244, 244, 245, 0.6);
  border-radius: 6px;
}

Example demo

Next steps