Skip to main content
useSuperDocTrackChanges() exposes the live tracked-change list in document order. Accept, reject, accept-all, reject-all, scroll-to, and next / previous navigation route through editor.doc.trackChanges.* underneath.

A live list of changes

import { useSuperDocTrackChanges, useSuperDocUI } from 'superdoc/ui/react';

export function ReviewPanel() {
  const { items, total, authors } = useSuperDocTrackChanges();
  const ui = useSuperDocUI();

  return (
    <aside>
      <h2>Track changes · {total}</h2>
      <div>
        {authors.map((author) => (
          <span key={`${author.email ?? ''}:${author.name ?? ''}`}>
            <span style={{ background: author.color }} />
            {author.name ?? author.email}
          </span>
        ))}
      </div>
      {items.map((item) => (
        <ChangeRow
          key={item.id}
          change={item.change}
          authorColor={item.authorColor}
          onAccept={() => ui?.trackChanges.accept(item.id)}
          onReject={() => ui?.trackChanges.reject(item.id)}
        />
      ))}
    </aside>
  );
}
items mirrors editor.doc.trackChanges.list(). Each item carries id plus the full change record (type, author, excerpt, address). If you configure modules.trackChanges.authorColors, each item also exposes authorColor, and authors contains the unique authors in document order with their resolved colors.

Per-author colors

Use modules.trackChanges.authorColors when your review UI needs a stable legend or when imported DOCX files can contain authors your app did not know ahead of time.
<SuperDocEditor
  document="/contract.docx"
  modules={{
    trackChanges: {
      visible: true,
      authorColors: {
        overrides: {
          'alice@example.com': '#1f6feb',
          'Bob Reviewer': '#d1242f',
        },
        resolve: (author) => {
          if (author.email?.endsWith('@outside-counsel.com')) return '#8250df';
          return undefined;
        },
      },
    },
  }}
  hideToolbar
  contained
  onReady={({ superdoc }) => setSuperDoc(superdoc)}
/>
SuperDoc uses the same resolver for the rendered document and the UI snapshot. That keeps custom cards, author legends, and document highlights in sync without app-side CSS selectors.

Accept and reject

ui.trackChanges.accept(changeId);
ui.trackChanges.reject(changeId);
ui.trackChanges.acceptAll();
ui.trackChanges.rejectAll();
All four return Document API receipts. After a single decision, the change leaves the live list (it’s been applied or discarded). To render an audit trail, snapshot the change before deciding:
const decideChange = (id: string, decision: 'accepted' | 'rejected') => {
  const item = trackChanges.items.find((it) => it.id === id);
  const snapshot = item?.change ?? null;

  if (decision === 'accepted') ui.trackChanges.accept(id);
  else ui.trackChanges.reject(id);

  if (snapshot) {
    setDecided((prev) => [...prev, { id, decision, snapshot }]);
  }
};
const nextId = ui.trackChanges.next();
const prevId = ui.trackChanges.previous();
ui.trackChanges.scrollTo(changeId);
next and previous walk the list in document order. They wrap. scrollTo scrolls the editor viewport to the change’s anchor and sets it as the active id.

Independent vs paired replacements

A typed-over selection in Suggest mode produces an insertion AND a deletion. Default 'paired' mode collapses them into one tracked-change entity (accept once, both apply). Independent mode gives each half its own id.
<SuperDocEditor
  document="/contract.docx"
  modules={{ trackChanges: { replacements: 'independent' } }}
  hideToolbar
  contained
  onReady={({ superdoc }) => setSuperDoc(superdoc)}
/>
Pick what your review UI wants to render. One row per replacement (paired) or two rows for the deletion + insertion (independent).

Highlight the active card

The selection slice exposes activeChangeIds. Use it to highlight the card matching the cursor.
import { useSuperDocTrackChanges, useSuperDocSelection } from 'superdoc/ui/react';

function ActiveAwareList() {
  const { items } = useSuperDocTrackChanges();
  const selection = useSuperDocSelection();
  const activeId = selection.activeChangeIds[0] ?? null;

  return items.map((item) => (
    <div key={item.id} className={item.id === activeId ? 'active' : ''}>
      {/* row */}
    </div>
  ));
}

What the snapshot looks like

FieldTypeMeaning
itemsTrackChangesItem[]Tracked changes in document order.
totalnumberConvenience count of items.length.
activeIdstring | nullActive change driven by selection or by next / previous / scrollTo.
authorsTrackChangesAuthor[]Unique tracked-change authors in document order. Present with resolved color values when modules.trackChanges.authorColors is configured.
TrackChangesItem is { id, change, authorColor? }. The change shape mirrors editor.doc.trackChanges.list(): type, author, authorEmail, excerpt, address, etc. When author colors are configured, change.authorColor mirrors item.authorColor.

Theming

Insertion, deletion, and format-change highlights are themable via --sd-tracked-changes-* CSS variables (--sd-tracked-changes-insert-background, --sd-tracked-changes-delete-border, etc.). Per-author colors set those variables on the rendered tracked-change element. See Theming overview and Custom themes for the full token list.

Trade-offs

  • acceptAll and rejectAll apply across every story. To scope to body only, call accept / reject per id.
  • Tracked changes in headers, footers, and footnotes route correctly through scrollTo. Non-body entities snap to view (story activation mounts the surface synchronously before alignment); body entities scroll smoothly.
  • The merged Activity feed pattern is consumer-owned. The controller stays minimal so apps that only render tracked changes don’t pay for comment merging, and apps that want an Activity panel decide their own ordering rules.