Track changes (redlines) extension for TipTap v3. Adds Microsoft Word-style revision tracking to any TipTap editor — insertions are highlighted with color and underline, deletions are preserved with strikethrough.
Built for TipTap v3. Based on chenyuncai/tiptap-track-change-extension, rewritten to use TipTap v3's addStorage() API instead of the v2 extension-lookup pattern.
- Insertion tracking — New text is wrapped in
<insert>tags - Deletion tracking — Deleted text is preserved and wrapped in
<delete>tags (strikethrough) - Accept/Reject — Accept or reject individual changes or all changes at once
- User attribution — Attach user ID and nickname to each change
- CJK input support — Handles IME composition for Chinese/Japanese/Korean input
- Y.js compatible — Ignores changes from collaborative sync
npm install tiptap-redlines-extensionPeer dependencies:
npm install @tiptap/core @tiptap/pmimport { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { RedlinesExtension } from 'tiptap-redlines-extension'
const editor = new Editor({
extensions: [
StarterKit,
RedlinesExtension.configure({
enabled: false, // start with tracking disabled
onStatusChange: (enabled) => {
console.log('Track changes:', enabled ? 'ON' : 'OFF')
},
}),
],
content: '<p>Hello World</p>',
})// Enable
editor.commands.setTrackChangeStatus(true)
// Disable
editor.commands.setTrackChangeStatus(false)
// Toggle
editor.commands.toggleTrackChangeStatus()// Accept the change at cursor position or within selection
editor.commands.acceptChange()
// Reject the change at cursor position or within selection
editor.commands.rejectChange()
// Accept all changes in the document
editor.commands.acceptAllChanges()
// Reject all changes in the document
editor.commands.rejectAllChanges()editor.commands.updateOpUserOption('user-123', 'Jane Smith')Each tracked change stores data-op-user-id, data-op-user-nickname, and data-op-date attributes on the mark element.
The extension renders insertions as <insert> elements and deletions as <delete> elements. Add CSS to style them:
/* Insertions — blue underline */
.ProseMirror insert {
color: #2563EB;
text-decoration: underline;
text-decoration-color: #2563EB;
text-underline-offset: 2px;
}
/* Deletions — red strikethrough */
.ProseMirror delete {
color: #E11D48;
text-decoration: line-through;
text-decoration-color: #E11D48;
opacity: 0.7;
}Or use any colors that match your design system.
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { RedlinesExtension } from 'tiptap-redlines-extension'
function MyEditor() {
const [isTracking, setIsTracking] = useState(false)
const editor = useEditor({
extensions: [
StarterKit,
RedlinesExtension.configure({
enabled: false,
onStatusChange: setIsTracking,
}),
],
content: '<p>Start editing...</p>',
})
return (
<div>
<div className="toolbar">
<button onClick={() => editor?.commands.toggleTrackChangeStatus()}>
{isTracking ? 'Suggesting' : 'Editing'}
</button>
<button onClick={() => editor?.commands.acceptAllChanges()}>
Accept All
</button>
<button onClick={() => editor?.commands.rejectAllChanges()}>
Reject All
</button>
</div>
<EditorContent editor={editor} />
</div>
)
}<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import { RedlinesExtension } from 'tiptap-redlines-extension'
const isTracking = ref(false)
const editor = useEditor({
extensions: [
StarterKit,
RedlinesExtension.configure({
enabled: false,
onStatusChange: (status) => { isTracking.value = status },
}),
],
content: '<p>Start editing...</p>',
})
</script>
<template>
<div>
<button @click="editor?.commands.toggleTrackChangeStatus()">
{{ isTracking ? 'Suggesting' : 'Editing' }}
</button>
<EditorContent :editor="editor" />
</div>
</template>| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
false |
Whether track changes is enabled on init |
onStatusChange |
(enabled: boolean) => void |
undefined |
Callback when status changes |
dataOpUserId |
string |
'' |
User ID for change attribution |
dataOpUserNickname |
string |
'' |
User nickname for change attribution |
| Command | Description |
|---|---|
setTrackChangeStatus(enabled) |
Enable or disable tracking |
getTrackChangeStatus() |
Get current tracking status |
toggleTrackChangeStatus() |
Toggle tracking on/off |
acceptChange() |
Accept change at cursor/selection |
acceptAllChanges() |
Accept all changes |
rejectChange() |
Reject change at cursor/selection |
rejectAllChanges() |
Reject all changes |
updateOpUserOption(id, name) |
Set user info for changes |
| Export | Description |
|---|---|
RedlinesExtension |
Main extension (default export) |
InsertionMark |
TipTap Mark for insertions |
DeletionMark |
TipTap Mark for deletions |
MARK_INSERTION |
Mark name constant ('insertion') |
MARK_DELETION |
Mark name constant ('deletion') |
Insertions render as:
<insert data-op-user-id="..." data-op-user-nickname="..." data-op-date="...">new text</insert>Deletions render as:
<delete data-op-user-id="..." data-op-user-nickname="..." data-op-date="...">removed text</delete>When tracking is enabled:
- Typing new text — The
onTransactionhook detects new content via ProseMirrorReplaceStepand applies theinsertionmark - Deleting text — Instead of removing content, the extension re-adds it with the
deletionmark (red strikethrough) - Deleting tracked insertions — If you delete text that was already marked as an insertion, it's removed for real (since it was a pending suggestion)
- Accepting a change — Insertions: the mark is removed (text becomes normal). Deletions: the content is removed for real.
- Rejecting a change — Insertions: the content is removed. Deletions: the mark is removed (text is restored).
- TipTap v3 (
@tiptap/core ^3.0.0) - Works with
@tiptap/react,@tiptap/vue-3, and vanilla JS - Compatible with Y.js collaborative editing (ignores sync changes)
Based on chenyuncai/tiptap-track-change-extension (MIT License). Rewritten for TipTap v3 compatibility using addStorage() instead of the v2 getSelfExt() pattern.
MIT