Word-style revision tracking. Every edit carries an author, a timestamp, and an id. Accept or reject one at a time, by selection, or in bulk. Changes round-trip through DOCX as native Word revisions — import, edit, export, nothing lost.
const superdoc = new SuperDoc({ selector: "#editor", document: "contract.docx", documentMode: "suggesting", // record new edits as tracked changes user: { name: "John Smith", email: "john@company.com", }, modules: { trackChanges: { visible: true, }, }, onCommentsUpdate: (payload) => { if (payload.type === "trackedChange") { console.log(payload.event, payload.trackedChangeType, payload.changeId); } },});
Tracked-edit recording follows documentMode. Set documentMode: 'suggesting' (or call superdoc.setDocumentMode('suggesting') later) to start recording new edits as revisions.
SuperDoc supports two models for how a tracked replacement (an insertion paired with a deletion, created when a user types over selected text) shows up in the API and UI. Pick the one that matches the editor your users expect.
Model
modules.trackChanges.replacements
Behavior
Paired (default — Google Docs)
'paired'
Both halves share one id. One accept/reject resolves both. One sidebar row per replacement.
Independent (Microsoft Word / ECMA-376 §17.13.5)
'independent'
Each insertion and each deletion has its own id. Accept/reject resolves one side at a time. A replacement produces two sidebar rows.
Both modes round-trip cleanly through DOCX — the OOXML always emits one <w:ins> / <w:del> per mark. The difference is how the API surfaces the revisions at runtime.
With replacements: 'independent', editor.doc.trackChanges.list() returns one entry per revision and decide({ id }) resolves exactly that one side. The other half of the replacement stays in the document, still addressable by its own id — useful when you’re building a custom sidebar and want each revision as a separate row.
Use the Document API to list, read, and resolve tracked changes. It’s stable, typed, framework-agnostic, and works the same in the visual editor and headless mode.
const editor = superdoc.activeEditor;// List every tracked change. In 'paired' mode a replacement is one entry;// in 'independent' mode it's two (one insert, one delete).const result = editor.doc.trackChanges.list();for (const item of result.items) { console.log(item.id, item.type, item.author, item.excerpt);}// Fetch a single change by id.const change = editor.doc.trackChanges.get({ id: result.items[0].id });// Accept or reject by id.editor.doc.trackChanges.decide({ decision: "accept", target: { id: change.id } });// Accept or reject everything in the document.editor.doc.trackChanges.decide({ decision: "accept", target: { scope: "all" } });
Every entry returned by list() and get() has this shape:
Revision kind. In 'paired' mode a replacement reports as 'insert' (both halves grouped). In 'independent' mode the insertion and deletion are separate entries.
superdoc.setDocumentMode("suggesting"); // record new edits as tracked changessuperdoc.setDocumentMode("editing"); // stop recordingsuperdoc.setDocumentMode("viewing"); // read-only
Each mark carries id, author, authorEmail, date, and — for imports from Word — the original w:id as sourceId so you can round-trip revision provenance.
Tracked-change events are delivered through the same onCommentsUpdate callback as comment events. The top-level type field tells them apart; filter on type === 'trackedChange' and read the flat payload.
onCommentsUpdate: (payload) => { if (payload.type !== "trackedChange") return; switch (payload.event) { case "add": // New tracked change created. break; case "update": // Existing tracked change updated (e.g. reply added, author edited). break; case "change-accepted": await markAccepted(payload.changeId); break; case "change-rejected": await markRejected(payload.changeId); break; case "resolved": // Tracked-change comment thread resolved. break; }};
Original author info from the imported DOCX, when available — { name }.
Events fire once per user action, not once per mark. A tracked replacement in paired mode emits one event with trackedChangeType: 'both'. To enumerate the current set of revisions, use editor.doc.trackChanges.list() — not the event stream.
Tracked changes round-trip through DOCX as native Word revisions. Import it. Edit it. Export it. Nothing lost.
// Export with revisions preserved. Word opens the file and shows the// insertions and deletions under Review.const blob = await superdoc.export();// Accept all first, then export a clean copy.superdoc.activeEditor.doc.trackChanges.decide({ decision: "accept", target: { scope: "all" },});const cleanBlob = await superdoc.export();
Imported Word revisions preserve their original w:id values as wordRevisionIds on each TrackChangeInfo entry, so you can correlate SuperDoc revisions with the source document or an external review system.
Round-trip support today covers inserted run content (<w:ins>), deleted run content (<w:del>), and run-level format changes (<w:rPrChange>). Paragraph-level property changes, tracked table row and cell edits, and tracked moves are on the roadmap — they import as accepted content today.
Deprecated. Use the Document API (editor.doc.trackChanges.list(), editor.doc.trackChanges.decide(...)) instead. The commands below remain available but will be removed in a future release.
These legacy commands live on superdoc.activeEditor.commands and predate the Document API. They’re still used by the built-in toolbar and a handful of keyboard shortcuts.
Temporarily render the document without applying revisions — useful for previewing the accepted or original state:
// Show the document as it was before tracked changes.superdoc.activeEditor.commands.enableTrackChangesShowOriginal();superdoc.activeEditor.commands.disableTrackChangesShowOriginal();superdoc.activeEditor.commands.toggleTrackChangesShowOriginal();// Show the document with all changes accepted.superdoc.activeEditor.commands.enableTrackChangesShowFinal();superdoc.activeEditor.commands.toggleTrackChangesShowFinal();