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 valueEngine stateBehavior
'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. can reports the same reason. The toolbar disables the Suggesting item and shows the reason. Other permitted modes stay available.
  • A mode="suggesting" prop without an author opens in editing mode.
  • Each case publishes the reason as lastRejection in the editor state.
  • The editor raises the configuration error once per instance through the error event, with the code suggestingNeedsAuthor, 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>
  );
}

Tracked content

ChangeSupport and display
TextTracks insertions, deletions, and replacements. A replacement uses one card.
Paragraph structureTracks inserted and deleted paragraph marks.
Paragraph propertiesTracks alignment, indents, spacing, and style in a separate card.
TablesTracks row and cell insertion, deletion, and property changes.
Run formattingTracks formatting changes in a separate card.
ImagesTracks 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.

PropDefaultBehavior
filterNoneReturns a subset of items.
structuralfalseAdds cards for structural revisions.
formattingfalseAdds cards for formatting revisions.
stacktrueMoves overlapping cards to prevent collisions.
gap8Sets the CSS-pixel gap between stacked cards.
furnitureNoneAdds host content before cards.
presettrueSet 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.

PartsPurposeasChildicon
ListBuilds the card collection.NoNo
CardWraps one review item.YesNo
EmptyShows the empty state.NoNo
Avatar, Author, Time, SummaryDisplay item metadata.YesNo
Accept, RejectResolve a revision.YesYes
Resolve, ReopenChange comment thread state.YesYes
DeleteDeletes a thread or discards a suggestion.YesYes
RepliesDisplays existing replies.NoNo
ReplyAdds a reply.NoNo
MarkersShows markers while the pane is closed.NoFunction or node
BalloonShows formatting or structural decisions.NoNo
AddCommentStarts a comment draft.NoNo
DraftAuthors a new comment.NoNo

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.

MemberBehavior
itemsPending decisions in reading order
activeKey, setActiveRead or open an item
accept, rejectResolve a revision
resolve, reopenChange comment thread state
replyAdd a reply
removeDelete a thread or discard a suggestion
commentComment on the current selection
selectionAnchorYProposed comment position, or null
paneOpen, setPaneOpenRead or change pane state
readyfalse before a document loads
commentResolutionDisabledReasonEngine refusal for comment state actions

Render item.text as text. The document controls this value. Do not render it as markup.

Item field groupFields
Identitykey, id, kind, author, initials, date
Contenttext, replyIds
PositionanchorY, pageIndex
StatereadOnly, activatable, isActive
Revision onlyrevisionKind, 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 valueScroll behavior
OmittedCenters an item only when scrolling is required.
'start'Places the item near the viewport start.
'nearest'Uses the minimum scroll distance.
falseOpens 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 exclusionEffect
excludeRevisionKindsRemoves those kinds from returned items.
placement: falseKeeps metadata and sets placement fields to null.
setReviewActivationExclusionsPrevents caret-driven activation for hidden kinds.
DocxEditorReviewSets 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 it

The 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 kindaccept modereject mode
insert, moveToProposed content remains as ordinary content.Proposed content is omitted.
delete, moveFromDeleted content is omitted.Deleted content returns as ordinary content.
replaceReplacement remains; replaced content is omitted.Replacement is omitted; replaced content returns.
paragraphMark, inserted/deleted rowAccepted structure is rendered.Original structure is rendered.
format, other structural/propertyCurrent values remain without tracked-change markup.Current values remain without tracked-change markup.

Predicate data

The predicate can use the following revision data:

FieldDescription
authorReviewer name stored in the DOCX revision.
dateOptional raw OOXML timestamp. Validate it before date comparisons.
revisionKindInsert, delete, replace, move, format, paragraph-mark, or structural change.
text, replacedTextProposed text and, for replacements, the text being replaced.
rangesAll document ranges covered by the decision, including part names.
address, addressesThe OOXML revision address or addresses resolved together.
nesting, pairedWith, markDirectionNesting, move/replacement pairing, and paragraph-mark metadata.
readOnlyWhether the engine can accept or reject this revision.
id, replacedRangeCount, replyIdsStable 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;
}

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 markup

If 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

WorkflowBehavior
SaveWrites standard w:ins and w:del with author and date.
OpenKeeps Word revisions and shows them in the page and sidebar.
Accept or reject in WordResolves revisions saved by the editor.
Other storiesTracks headers, footers, footnotes, and endnotes in their scopes.

Next steps

On this page