Tracked changes
Use suggesting mode to record Word revisions. Render, accept, or reject them with the review sidebar or your own interface.
Suggesting mode records edits as Word revisions. The editor underlines insertions and strikes through deletions.
Each revision keeps available author and timestamp metadata. OOXML timestamps are optional. Saved files use Word w:ins and w:del markup.
This feature requires the review module from @docx-editor.dev/pro.
Review changes
Use Review > Next Change or Previous Change to move between visible changes. Navigation skips comments and hidden authors, and wraps at the end. A continuous insertion stays in one card when it crosses field markers or tracked paragraph breaks. Accepting or rejecting that card resolves its field instructions, results, and surrounding text together. Accepting or rejecting a card preserves the caret. Use Next Change to continue review.
Use Review > Accept all changes shown or Reject all changes shown to resolve changes selected by your active filters. These controls include offscreen changes in supported stories and leave hidden or unsupported changes pending. A successful operation preserves comments and creates one undo step. To resolve all authors or require every selected change to resolve, configure the bulk review command.
Formatting cards describe changed language, bold, italic, underline, strike, font, size, color, alignment, indentation, and paragraph spacing values. Separate formatting decisions remain independently reviewable even when their OOXML revision IDs match.
Review > Display for Review offers Simple Markup, All Markup, No Markup, and Original. These views preserve the document and its pending changes. Simple Markup shows the proposed text with a red change bar beside each changed line. Original restores prior run and paragraph formatting. Prior table, row, cell, and section formatting remains unsupported.
To offer a view from your own chrome, drive the review.simpleMarkup, review.allMarkup, review.noMarkup, or review.original slot with useEditorCommand, and read the current view with useEditorState:
import { useEditorCommand, useEditorState } from '@docx-editor.dev/react';
function SimpleMarkupButton() {
const view = useEditorState((state) => state.reviewDisplayMode);
const simple = useEditorCommand('review.simpleMarkup');
return (
<button
type="button"
onClick={simple.execute}
disabled={!simple.isEnabled}
aria-pressed={view === 'simple-markup'}
>
Simple Markup
</button>
);
}reviewDisplayMode takes one of four values. Word models the same views as a markup setting and a view setting:
| Word menu item | reviewDisplayMode | Word RevisionsMarkup | Word RevisionsView |
|---|---|---|---|
| Simple Markup | 'simple-markup' | Simple | Final |
| All Markup | 'all-markup' | All | Final |
| No Markup | 'proposed' | None | Final |
| Original | 'original' | None | Original |
A click on a change bar changes the value too. Vue offers the same pair of composables.
Choose an editing mode
Root mode value | Engine state | Behavior |
|---|---|---|
'edit' | 'editing' | Applies edits without revisions. |
'suggesting' | 'suggesting' | Records supported edits as revisions. |
'view' | 'viewing' | Keeps the document read-only. |
Use the setEditingMode command to change the mode after mount. The packaged toolbar exposes the same command in review.editingMode.
If you omit mode, the root honors w:trackRevisions when a review module and an author are present. An explicit mode overrides this document setting.
The <DocxEditor> convenience component defaults to mode="edit".
Tracked-changes protection requires suggesting mode for edits. The review module and author are still required. Forms and comments-only protection refuse suggesting mode; read-only protection opens in viewing mode.
Turning on Review > Protect Document for Forms while suggesting switches to editing mode. snapshot().lastRejection explains the change. For allowed edits, password limits, and custom controls, see Document protection.
Suggesting needs an author
A revision records who proposed it, so suggesting mode needs an author. If you enable suggesting without one, the editor refuses the request instead of accepting focus and ignoring keystrokes:
setEditingMode('suggesting')returns{ ok: false, code: 'invalidArgs' }with the reason.canreports the same reason. The toolbar disables the Suggesting item and shows the reason. Other permitted modes stay available.- A
mode="suggesting"prop without anauthoropens in editing mode. - Each case publishes the reason as
lastRejectionin the editor state. - The editor raises the configuration error once per instance through the
errorevent, with the codesuggestingNeedsAuthor, and logs the same message to the console once.
Set the author at mount, or later with setAuthor or the author prop. When the author arrives, the pending request enters suggesting mode, unless the reader has already chosen a mode. Calls to setEditingMode count as reader choices, including calls from onReady.
If you remove the author while mode="suggesting" is active, the editor returns to editing mode and publishes the reason, unless document protection forbids editing. Suggesting adopted from a document or chosen through setEditingMode stays active, refuses edits, and publishes the reason until the author returns.
This example switches between editing and suggesting:
import { DocxEditor, useEditorCommand, useEditorState } from '@docx-editor.dev/react';
import { DocxEditorReview, reviewModule } from '@docx-editor.dev/pro/react';
const MODULES = [reviewModule()];
function SuggestToggle() {
const mode = useEditorState((state) => state.editingMode);
const suggest = useEditorCommand({ type: 'setEditingMode', mode: 'suggesting' });
const edit = useEditorCommand({ type: 'setEditingMode', mode: 'editing' });
const active = mode === 'suggesting';
return (
<button
onMouseDown={(event) => event.preventDefault()}
onClick={() => (active ? edit : suggest).execute()}
>
{active ? 'Suggesting' : 'Editing'}
</button>
);
}
export function Reviewer({ bytes }: { bytes: Uint8Array }) {
return (
<DocxEditor.Root document={bytes} modules={MODULES} author="Jess Lin">
<SuggestToggle />
<DocxEditor.Viewport>
<DocxEditor.Content />
<DocxEditorReview />
</DocxEditor.Viewport>
</DocxEditor.Root>
);
}<!-- SuggestToggle.vue -->
<script setup lang="ts">
import { useEditorCommand, useEditorState } from '@docx-editor.dev/vue';
const mode = useEditorState((state) => state.editingMode);
const suggest = useEditorCommand({ type: 'setEditingMode', mode: 'suggesting' });
const edit = useEditorCommand({ type: 'setEditingMode', mode: 'editing' });
</script>
<template>
<button
type="button"
@mousedown.prevent
@click="mode === 'suggesting' ? edit.execute() : suggest.execute()"
>
{{ mode === 'suggesting' ? 'Suggesting' : 'Editing' }}
</button>
</template><!-- Reviewer.vue -->
<script setup lang="ts">
import { DocxEditorContent, DocxEditorRoot, DocxEditorViewport } from '@docx-editor.dev/vue';
import { DocxEditorReview, reviewModule } from '@docx-editor.dev/pro/vue';
import SuggestToggle from './SuggestToggle.vue';
defineProps<{ bytes: Uint8Array }>();
const modules = [reviewModule()];
</script>
<template>
<DocxEditorRoot :document="bytes" :modules="modules" author="Jess Lin">
<SuggestToggle />
<DocxEditorViewport>
<DocxEditorContent />
<DocxEditorReview />
</DocxEditorViewport>
</DocxEditorRoot>
</template>Tracked content
| Change | Support and display |
|---|---|
| Text | Tracks insertions, deletions, and replacements. A replacement uses one card. |
| Paragraph structure | Tracks inserted and deleted paragraph marks. |
| Paragraph properties | Tracks alignment, indents, spacing, and style in a separate card. |
| Tables | Tracks row and cell insertion, deletion, and property changes. |
| Run formatting | Tracks formatting changes in a separate card. |
| Images | Tracks insertion and deletion. Property edits are unavailable. |
In suggesting mode, a formatting change keeps the new properties and records the old ones, so a reviewer can put them back. A run records w:rPrChange, a paragraph mark records w:pPr/w:rPr/w:rPrChange, and paragraph properties record w:pPrChange. Accept drops the record; reject restores what it holds.
One press is one card, however many runs the selection covers. The record holds the properties each run started with, so a second press on the same run adds no second card. Setting a property back to the value your own record holds removes that record.
A toggle pressed twice is not that case. Turning bold off writes an explicit off value rather than removing the property, because the property might come from a style. The run's properties therefore differ from the ones it started with, and the change is recorded.
Formatting text inside your own pending insertion records nothing: the whole run is already your proposal, so rejecting the insertion takes the words and the formatting together. Formatting text inside another author's pending insertion does record a change, and that record is yours. Their insertion is untouched.
If your change lands on the properties another author's record holds, their record stays. Resolving their proposal is a review decision, so take it in the review pane rather than through a formatting press.
Lists, indent level, and tab stops are not recorded. Changing them in suggesting mode applies the change with no card. Table property changes made in the editor are not recorded either; a w:tblPrChange a file already carries still renders.
A document that sets w:doNotTrackFormatting in settings.xml gets no formatting records. Its text edits stay tracked.
Paragraph marks stay hidden until Review → Show paragraph breaks is enabled, even when a review card is selected. Use the Review menu, Cmd+8 on Mac, or Ctrl+Shift+8 on Windows. This changes the view only, including ordinary paragraph ends (¶) and manual line breaks (↵, Shift+Enter). Tracked line-break marks use their revision color and decoration. Automatic line wraps, page breaks, and column breaks do not receive a line-break arrow. Showing these symbols does not change copied text or saved documents. Adjacent inserted text and breaks by the same author form one review decision. A decision containing only breaks shows their count.
Change bars
The editor draws Word's change bar: a rule in the left margin beside every line that carries a tracked change. The bar sits halfway into the margin, whatever the paragraph indent, and one bar covers adjacent changed lines and paragraphs, including their paragraph spacing. Body text, tables, headers, footers, footnotes, endnotes, and text boxes all get the bar, and so do formatting-only changes and tracked table rows.
In All Markup the bar is a neutral gray hairline, as in Word. In Simple Markup the text reads as the proposed result, with no underlines, strikes, or author colors, and the bar is red and heavier beside every line a change touched, including a deleted word that is no longer shown. A click on the bar switches between Simple Markup and All Markup, as in Word. The resolved views, No Markup and Original, draw no bar. To change the bar's color or width, or to color it by change kind, see Review colors and styling.
Mirrored margins do not move the bar to the outside edge.
One paragraph mark can contain two decisions. For example, one author can insert a break before another author suggests its removal.
Attribution appears only in All Markup. Resolved views omit review colors and change bars. The same setting controls ordinary paragraph marks. No Markup merges paragraphs when a revision deletes their paragraph mark.
For author colors and avatars, see Review colors and styling.
Add the review sidebar
<DocxEditorReview /> renders one card for each included pending decision. Place it inside the viewport so the rail scrolls with the document.
The default sidebar shows content changes and comments. Formatting and structural changes remain available in page balloons: click the changed text or paragraph to inspect them. Set formatting or structural to include those cards in the sidebar as well.
| Prop | Default | Behavior |
|---|---|---|
filter | None | Returns a subset of items. |
structural | true | Shows cards for structural revisions. |
formatting | false | Shows cards for formatting revisions. |
stack | true | Moves overlapping cards to prevent collisions. |
gap | 8 | Sets the CSS-pixel gap between stacked cards. |
furniture | None | Adds host content before cards. |
preset | true | Set false to keep context and anchors without packaged cards. |
Structural and formatting revisions remain marked when their cards are hidden. Selecting one opens its balloon.
Review parts
All listed parts accept className and hidden. The other supported props vary by part.
| Parts | Purpose | asChild | icon |
|---|---|---|---|
List | Builds the card collection. | No | No |
Card | Wraps one review item. | Yes | No |
Empty | Shows the empty state. | No | No |
Avatar, Author, Time, Summary | Display item metadata. | Yes | No |
Accept, Reject | Resolve a revision. | Yes | Yes |
Resolve, Reopen | Change comment thread state. | Yes | Yes |
Delete | Deletes a thread or discards a suggestion. | Yes | Yes |
Replies | Displays existing replies. | No | No |
Reply | Adds a reply. | No | No |
Markers | Shows markers while the pane is closed. | No | Function or node |
Balloon | Shows formatting or structural decisions. | No | No |
AddComment | Starts a comment draft. | No | No |
Draft | Authors a new comment. | No | No |
Markers accepts an icon function because one component draws all markers. The packaged icon identifies the item kind.
On a comment card, Delete removes the thread. On a tracked-change card, it discards the suggestion. Reply delete controls remove only their reply.
The stylesheet shows delete controls on hover or keyboard focus. Cards without a deletable target omit the control.
Build a custom review interface
useReview() returns the same data and actions as the packaged rail.
React returns plain values from useReview(). Vue returns computed refs for items, activeKey, ready, paneOpen, selectionAnchorY, and commentResolutionDisabledReason. Vue templates unwrap these refs.
| Member | Behavior |
|---|---|
items | Pending decisions in reading order |
activeKey, setActive | Read or open an item |
accept, reject | Resolve a revision |
resolve, reopen | Change comment thread state |
reply | Add a reply |
remove | Delete a thread or discard a suggestion |
comment | Comment on the current selection |
selectionAnchorY | Proposed comment position, or null |
paneOpen, setPaneOpen | Read or change pane state |
ready | false before a document loads |
commentResolutionDisabledReason | Engine refusal for comment state actions |
Render item.text as text. The document controls this value. Do not render it as markup.
| Item field group | Fields |
|---|---|
| Identity | key, id, kind, author, initials, date |
| Content | text, replyIds |
| Position | anchorY, pageIndex |
| State | readOnly, activatable, isActive |
| Revision only | revisionKind, replacedText |
The nested item field holds engine review data. For an entry, revision ranges are in entry.item.ranges. Comment ranges are in entry.item.range.
A reply to a revision creates a comment over its range. Its parentRevisionId links it to the revision. Omit comments with parentId or parentRevisionId from top-level lists.
readOnly means the engine cannot resolve that item. Hide Accept and Reject in custom cards. Packaged actions remain visible but disabled in viewing mode.
Activate and reveal items
setActive moves the caret to the item start and opens its story. It does not select content. The item renders its own highlight.
reveal value | Scroll behavior |
|---|---|
| Omitted | Centers an item only when scrolling is required. |
'start' | Places the item near the viewport start. |
'nearest' | Uses the minimum scroll distance. |
false | Opens the item without scrolling. |
setActive returns false when activation is excluded, no range exists, or the story cannot open.
Use useReviewOf(editor, query) with an existing editor. React accepts Editor | null. Vue requires Ref<Editor | null> and accepts a reactive query.
| Query or exclusion | Effect |
|---|---|
excludeRevisionKinds | Removes those kinds from returned items. |
placement: false | Keeps metadata and sets placement fields to null. |
setReviewActivationExclusions | Prevents caret-driven activation for hidden kinds. |
DocxEditorReview | Sets activation exclusions from structural and formatting. |
A query filters returned data. It does not limit caret-driven activation.
Filter by reviewer
The packaged menu exposes Review → Markup Options → Reviewers. The default toolbar omits the shortcut to keep the chrome compact. A host that wants one can compose DocxEditor.Toolbar.Reviewers and provide a custom icon (or the Vue default slot). Each checked author keeps their tracked markup and review cards visible. Clearing an author shows that author’s revisions as accepted: insertions remain as ordinary text, deletions leave the layout, and formatting stays applied without markup. Comments by that author are hidden.
Reviewer visibility is view-only. It does not accept changes or alter saved DOCX content, and showing the author again restores their markup. Bulk actions over useReview().items affect only the items currently returned by the filtered review list.
Use editor.getReviewAuthors() to build custom chrome. Read visibility with editor.isReviewAuthorVisible(author), change one author with editor.setReviewAuthorVisible(author, visible), or call editor.setAllReviewAuthorsVisible(visible) and editor.showAllReviewAuthors().
Filter tracked changes with a predicate
Use a tracked-changes predicate when reviewer names are not enough—for example, to show only recent changes, only deletions, or a combination of author, date, kind, and document location. The API is view-only:
import type { TrackedChangeFilterMode, TrackedChangePredicate } from '@docx-editor.dev/core/editor';
const predicate: TrackedChangePredicate = (revision) => true;
editor.setTrackedChangesFilter(predicate); // install or re-evaluate a filter
editor.setTrackedChangesFilter(predicate, 'reject'); // show excluded revisions as rejected
editor.setTrackedChangesFilter(null); // clear itThe predicate receives one complete ReviewRevisionItem for each revision decision. Return true to keep the revision as tracked markup and in the review list. Return false to remove its revision card and render it with the selected mode. The default mode is accept, matching Word's Show Markup → Specific People behavior.
| Revision kind | accept mode | reject mode |
|---|---|---|
insert, moveTo | Proposed content remains as ordinary content. | Proposed content is omitted. |
delete, moveFrom | Deleted content is omitted. | Deleted content returns as ordinary content. |
replace | Replacement remains; replaced content is omitted. | Replacement is omitted; replaced content returns. |
paragraphMark, inserted/deleted row | Accepted structure is rendered. | Original structure is rendered. |
format, other structural/property | Current values remain without tracked-change markup. | Current values remain without tracked-change markup. |
Predicate data
The predicate can use the following revision data:
| Field | Description |
|---|---|
author | Reviewer name stored in the DOCX revision. |
date | Optional raw OOXML timestamp. Validate it before date comparisons. |
revisionKind | Insert, delete, replace, move, format, paragraph-mark, or structural change. |
text, replacedText | Proposed text and, for replacements, the text being replaced. |
ranges | All document ranges covered by the decision, including part names. |
address, addresses | The OOXML revision address or addresses resolved together. |
nesting, pairedWith, markDirection | Nesting, move/replacement pairing, and paragraph-mark metadata. |
readOnly | Whether the engine can accept or reject this revision. |
id, replacedRangeCount, replyIds | Stable decision identity and additional review-card metadata. |
Dates are document data and can be absent or invalid. Use Date.parse together with Number.isFinite before comparing them.
Install a filter
This example keeps only insertions from Jess on or after February 1, 2026:
import { useEffect } from 'react';
import { useDocxEditor } from '@docx-editor.dev/react';
const CUTOFF = Date.parse('2026-02-01T00:00:00Z');
export function RecentJessInsertions() {
const editor = useDocxEditor();
useEffect(() => {
if (!editor) return;
editor.setTrackedChangesFilter((revision) => {
const timestamp = Date.parse(revision.date ?? '');
return (
revision.author === 'Jess' &&
revision.revisionKind === 'insert' &&
Number.isFinite(timestamp) &&
timestamp >= CUTOFF
);
});
return () => editor.setTrackedChangesFilter(null);
}, [editor]);
return null;
}<script setup lang="ts">
import { watch } from 'vue';
import { useDocxEditor } from '@docx-editor.dev/vue';
const CUTOFF = Date.parse('2026-02-01T00:00:00Z');
const editor = useDocxEditor();
watch(
editor,
(current, _previous, onCleanup) => {
if (!current) return;
current.setTrackedChangesFilter((revision) => {
const timestamp = Date.parse(revision.date ?? '');
return (
revision.author === 'Jess' &&
revision.revisionKind === 'insert' &&
Number.isFinite(timestamp) &&
timestamp >= CUTOFF
);
});
onCleanup(() => current.setTrackedChangesFilter(null));
},
{ immediate: true }
);
</script>Common recipes
Predicates are ordinary functions, so compose small rules with boolean logic:
import type { TrackedChangePredicate } from '@docx-editor.dev/core/editor';
const byJess: TrackedChangePredicate = (revision) => revision.author === 'Jess';
const deletionsOnly: TrackedChangePredicate = (revision) =>
revision.revisionKind === 'delete' || revision.revisionKind === 'moveFrom';
const inMainDocument: TrackedChangePredicate = (revision) =>
revision.ranges.some((range) => range.partName === '/word/document.xml');
editor.setTrackedChangesFilter(
(revision) => byJess(revision) && deletionsOnly(revision) && inMainDocument(revision)
);Choose how excluded revisions render
Pass accept to temporarily apply revisions that return false. Pass reject to render the rejected result for content revisions, moves, paragraph marks, and inserted or deleted table rows. Both modes are projections only: changing the mode restores the other view immediately, and saving preserves the canonical revision markup.
const mode: TrackedChangeFilterMode = 'reject';
editor.setTrackedChangesFilter((revision) => revision.author === 'Jess', mode);The mode applies only to revisions excluded by the predicate. Authors unchecked in Review → Markup Options → Reviewers continue to use Word's accepted projection, even when the predicate uses reject. Formatting revisions and structural property-change records do not carry a separate layout projection; in either mode their card and markup hide while their current values remain painted. Use the normal reject action to restore supported formatting revisions. Read-only structural property records cannot be restored by the engine.
The predicate filter and the reviewer menu compose. A revision stays tracked only when its author is checked under Review → Markup Options → Reviewers and the predicate returns true. The predicate runs for revisions, not comments. Comments remain visible unless the reviewer menu hides their author.
Update and clear a filter
The editor evaluates the predicate once per revision item when the filter or document changes and caches the decisions for pagination. Keep it synchronous, deterministic, and free of side effects. If the function closes over mutable state, call setTrackedChangesFilter again after that state changes—even when you reuse the same function reference:
let visibleKinds = new Set(['insert', 'replace']);
const predicate: TrackedChangePredicate = (revision) => visibleKinds.has(revision.revisionKind);
editor.setTrackedChangesFilter(predicate);
visibleKinds = new Set(['delete']);
editor.setTrackedChangesFilter(predicate); // re-evaluate the captured state
editor.setTrackedChangesFilter(null); // restore all tracked markupIf evaluation throws, the editor preserves the last complete projection and surfaces the error. Clear the predicate during component cleanup so a filter does not outlive the UI that owns it.
Saving and review actions
Filtering changes layout, painted markup, review cards, and bulk actions over the visible review list. The accept and reject modes do not accept or reject revisions, mutate the document, or change saved OOXML. Saving while a filter is active writes the same canonical revision markup as saving with no filter. To change the document, call the normal accept or reject actions explicitly.
If preset={false}, use useStackedReviewPositions(items, heights, { gap, scale }). Pass editor.getRenderScale() as scale. Card heights use CSS pixels. Anchors use document points.
Accept or reject in bulk
The review hook has no accept-all command. Call the per-item action for each actionable revision.
import { DocxEditor, useEditorState } from '@docx-editor.dev/react';
import { DocxEditorReview, reviewModule, useReview } from '@docx-editor.dev/pro/react';
const MODULES = [reviewModule()];
function AcceptAll() {
const { items, accept } = useReview();
const viewing = useEditorState((state) => state.editingMode === 'viewing');
const actionable = items.filter((item) => item.kind === 'revision' && !item.readOnly);
return (
<button
disabled={viewing || actionable.length === 0}
onMouseDown={(event) => event.preventDefault()}
onClick={() => actionable.forEach(accept)}
>
Accept all ({actionable.length})
</button>
);
}
export function BulkReviewer({ bytes }: { bytes: Uint8Array }) {
return (
<DocxEditor.Root document={bytes} modules={MODULES} author="Jess Lin">
<AcceptAll />
<DocxEditor.Viewport>
<DocxEditor.Content />
<DocxEditorReview />
</DocxEditor.Viewport>
</DocxEditor.Root>
);
}Each call resolves every location with the revision's (id, author, date) tuple. The call uses one transaction and one undo step. A tracked row insertion cannot become partly resolved.
The automation object model also provides revision collection bulk actions. See the editor API.
Detect review content without Pro
The snapshot field hasReviewContent detects revisions or comment anchors without a registered review module.
const hasReview = useEditorState((state) => state.hasReviewContent ?? false);Without the module, the editor renders revisions in their final state. It keeps the original revision markup during save.
Word compatibility
| Workflow | Behavior |
|---|---|
| Save | Writes standard w:ins and w:del with author and date. |
| Open | Keeps Word revisions and shows them in the page and sidebar. |
| Accept or reject in Word | Resolves revisions saved by the editor. |
| Other stories | Tracks headers, footers, footnotes, and endnotes in their scopes. |
Next steps
- Review colors and styling: Style review content.
- Comments: Add discussion threads.
- React composition: Arrange React review parts.
- Vue composition: Arrange Vue review parts.