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.
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". Enforced
document protection can also restrict the available mode.
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.
A changed paragraph mark shows a colored pilcrow and margin change bar. This display also works inside table cells.
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, pilcrows, and change bars. 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 excludes structural and formatting revisions. Its props can include those revisions or filter the queue further.
| Prop | Default | Behavior |
|---|---|---|
filter | None | Returns a subset of items. |
structural | false | Adds cards for structural revisions. |
formatting | false | Adds 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 today; 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 currently 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 currently 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.