Document refresh API

Display updated DOCX files, preserve scroll, and present recent changes in React and Vue.

Use createDocumentRefresh(editor) to display complete DOCX files returned by your server. The controller preserves the editor instance, subscriptions, and scroll position. It clamps scroll when the replacement is shorter. Highlighting and change navigation require explicit calls.

Each accepted file resets selection, undo history, and some review state. The controller refuses results after local edits and refuses collaborative sessions, including disconnected sessions. It does not merge edits or compare arbitrary documents. Ordinary editor.load() keeps its existing behavior and void return type.

The controller is a host integration API. It is separate from the Office.js document automation model. Keep processor jobs, credentials, and service sessions in your application backend.

Your application receives updates and calls refresh.applyUpdate(); the editor does not watch files or URLs. The DOCX document refresh example includes a React app and mock server.

API overview

Call createDocumentRefresh(editor) after the editor mounts. Repeated calls for the same editor return the same controller. It provides these methods:

MethodPurpose
capture()Capture the current DOCX bytes and revision before starting a server job. Returns a submission.
applyUpdate(update)Accept a complete DOCX using that submission and a processor-assigned sequence. Returns a success or refusal result.
finish(submission)Close the submission when the job ends. Accepted content and change navigation remain available.
cancel()Invalidate pending results. Cancel your network request separately.
highlightChanges(options?)Highlight recent available changes. Configure colors, decoration, fade timing, and display time. Returns the selected change count.
clearHighlights(options?)Dismiss highlights with a fade or remove them immediately.
navigateToChange(id, options?)Reveal a validated change location. Configure alignment, scrolling, and focus. Returns whether the location is available.
snapshot(), subscribe(listener), onResult(listener)Read status, observe state changes, and receive accepted or refused results.
recover(), recoveryBytes()Retry restoration after a failed replacement or obtain recovery bytes for download.

The update flow is capture() → your server job → applyUpdate()finish(). Call presentation methods after a successful applyUpdate(). Scroll preservation is automatic; highlighting and navigation are opt-in.

For integration code, see Accept an updated file.

Try the effects

This simulation demonstrates highlights, navigation, and refused updates with sample text. It does not load the editor or contact a server. The runnable examples use DOCX files and the public API.

Accept an updated file

Import the controller from your adapter or @docx-editor.dev/core/editor. Get the editor through useDocxEditor() inside the editor root. React returns an instance or null; Vue returns a shallow ref. Call the controller after the document mounts.

Capture the document before sending it to your backend:

import { createDocumentRefresh } from '@docx-editor.dev/react';

const refresh = createDocumentRefresh(editor);
const submission = await refresh.capture();

try {
  const response = await fetch('/api/update-document', {
    method: 'POST',
    body: submission.bytes,
  });
  if (!response.ok) throw new Error('Processing failed');

  const result = await refresh.applyUpdate({
    submission,
    sequence: 1,
    bytes: await response.arrayBuffer(),
  });
  if (result.ok) {
    refresh.highlightChanges();
  } else {
    showRefreshError(result.code);
  }
} finally {
  refresh.finish(submission);
}

The example can highlight returned tracked revisions when a review module supplies their locations. For plain DOCX files, provide change locations to enable highlights and navigation.

capture() finishes buffered input and form input before export. It waits for active text composition to finish. It captures bytes and their revision together. The document stays editable during external processing. Edits after capture make applyUpdate() return local-edits, even after undo. Submit the edited document again; do not replay the old result.

Keep the returned submission object in the browser. Pass that same object to applyUpdate(). A copied token, another editor's token, or a superseded token cannot authorize replacement. Send its bytes and ID to your backend when needed.

Receive cumulative updates

Assign each result a nonnegative integer sequence at the processor. Each file must contain the complete cumulative output for that submission. Use processor order, never network arrival order. The controller serializes replacement and rejects duplicate or older accepted sequences. It advances its baseline only after successful loading.

Apply each complete file with its processor-assigned sequence:

const submission = await refresh.capture();

try {
  for await (const update of receiveProcessorUpdates(submission.bytes)) {
    const result = await refresh.applyUpdate({
      submission,
      sequence: update.sequence,
      bytes: update.bytes,
      changes: update.changes,
      failures: update.failedOperationIds,
    });
    if (!result.ok) {
      showRefreshError(result.code);
      break;
    }
    refresh.highlightChanges();
    showPartialFailures(result.failures);
  }
} finally {
  refresh.finish(submission);
}

failures reports processor operation IDs without repeating successful work. The controller never retries processor operations. cancel() invalidates pending processing and cancels a deferred replacement. It cannot reverse an already accepted file. Cancel your application's network request separately.

Do not start processing from every document change event. Replacement events have source: 'refresh' or source: 'recovery'. Ordinary loads and internal remounts have source: 'load'. User edits omit source. Filter replacement events before scheduling automatic processing:

editor.on('change', (change) => {
  if (change.source) return;
  scheduleProcessing();
});

Provide reliable change locations

A plain replacement file does not identify its changes. Without metadata or returned tracked revisions, the result reports changeInformation: 'unavailable'. The controller does not infer change locations by comparing the two files.

Supply metadata for the returned file, using stable change IDs across cumulative results:

const result = await refresh.applyUpdate({
  submission,
  sequence: 2,
  bytes: updatedBytes,
  changes: [
    {
      id: 'delivery-date',
      location: {
        paragraphId: '00A12B34',
        start: 10,
        end: 20,
        text: 'October 12',
      },
    },
    { id: 'removed-clause', unavailableReason: 'deleted' },
  ],
});

paragraphId is the paragraph's OOXML w14:paraId. Alternatively, use paragraphIndex, starting at zero in body document order. This order includes paragraphs inside tables. Specify exactly one paragraph selector. Offsets count UTF-16 code units. The text must equal the text between start and end in the returned paragraph.

Locations cover nonempty ranges within one body paragraph. A missing, ambiguous, mismatched, or unplaced location does not receive a highlight. The result reports invalid, unavailable, deleted, or unsupported-story explicitly. Header, footer, note, and multiparagraph revision locations are unavailable for this presentation API. Use explicit metadata for a surviving body location when appropriate.

If you omit changes, a registered review module supplies tracked revision locations. Tracked revisions already present at capture do not count as new changes. Their preservation does not require a review module. Review module use requires the EigenPal Pro License.

Highlight and navigate

result.changes belongs to that result's resultId. isNew identifies new change IDs and changed text within an existing change ID. It compares cumulative results within one submission, plus tracked revisions present at capture. Plain metadata IDs do not form a history across independent captures. Locations remain valid only until the next accepted result or local edit. The controller clears stale highlights after a local edit.

Call highlightChanges() after a successful applyUpdate() to display highlights. These calls change presentation only:

refresh.highlightChanges(); // New available changes from the latest result.
refresh.highlightChanges({ includePrevious: true });
refresh.clearHighlights();

const available = refresh
  .snapshot()
  .changes.filter((change) => change.isNew && change.status === 'available');
if (available.length) {
  refresh.navigateToChange(available[0].id);
}

Highlights mark the paragraphs that contain the validated ranges. They do not change saved bytes or add undo entries. Navigation centers the target without animation or focus changes by default. Pass { focus: true } to move the caret and focus explicitly. Navigation returns false when the location is unavailable. Applications can cycle the available array for previous and next controls.

To scroll to a recent change before the fade starts, navigate first:

const target = refresh
  .snapshot()
  .changes.find((change) => change.isNew && change.status === 'available');

if (target && refresh.navigateToChange(target.id, { block: 'centerIfNeeded' })) {
  refresh.highlightChanges({
    changeIds: [target.id],
    timeoutMs: 3000,
    animation: { durationMs: 180 },
  });
}

The highlight fades in, remains visible, and fades out automatically. Both fades use animation.durationMs unless you set animation.exitDurationMs. Use this sequence after an explicit user action because it changes the scroll position.

Customize highlights and motion

The default highlight is a light blue, borderless box with rounded corners. It extends four pixels beyond the paragraph text area. It fades in over 180 milliseconds and starts fading out after three seconds. It does not move text, change scroll, or take focus.

highlightChanges() returns the number of selected, available changes, including offscreen locations. A zero result means no locations were selected. Options are validated even when locations are stale or unavailable. During document replacement, highlighting returns zero and navigation returns false. Completion listeners can present the accepted locations.

Pass presentation settings directly to highlightChanges(). Each call starts from the defaults, so reuse one options object for consistent styling:

import type { RefreshHighlightOptions } from '@docx-editor.dev/react';

const highlights: RefreshHighlightOptions = {
  color: 'var(--doc-refresh-highlight-color)',
  opacity: 0.14,
  padding: 4,
  borderRadius: 6,
  timeoutMs: 3000,
  animation: { durationMs: 180 },
};

refresh.highlightChanges(highlights);
refresh.highlightChanges({ ...highlights, includePrevious: true });
OptionDefaultMeaning
changeIdsOmittedSelect only these IDs, including earlier changes. Overrides includePrevious. Missing IDs are skipped. An empty array dismisses highlights.
colorvar(--doc-refresh-highlight-color)A CSS color or a CSS variable. The theme token defaults to blue.
opacity0.14Fill opacity between 0 and 1. Borders, shadows, and patterns retain their own color alpha.
padding4Extra space on each edge, in CSS pixels at 100% zoom.
borderRadius6Corner radius in CSS pixels at 100% zoom.
borderWidth0Border width in CSS pixels at 100% zoom.
borderColorSame as colorCSS border color, independent of fill opacity. Use an alpha color for a lighter border.
borderStyle'solid'Use 'solid', 'dashed', or 'dotted'.
classNameOmittedAdd CSS classes for shadows, outlines, or background patterns.
timeoutMs3000Milliseconds before dismissal starts. Use null to keep highlights until cleared.
animationtrueA 180ms opacity fade. Use false for immediate changes.
animation.durationMs180Entrance and default exit duration, from 0 to 10000 milliseconds.
animation.exitDurationMsSame as durationMsSeparate automatic and explicit exit duration, from 0 to 10000 milliseconds.
animation.easingTheme tokenCSS timing function for both fades. Accepts keywords, cubic-bezier(), and steps().

All numeric settings must be finite. Border width must be nonnegative. changeIds accepts at most 10000 nonempty string IDs. timeoutMs accepts 0 through 2147483647, or null. Padding and corner radius must be nonnegative. They scale with document zoom. Highlights exclude paragraph spacing before and after the text. Padding stops at the page edges and does not extend outside clipped text frames. Highlights cannot intercept clicks or enter saved DOCX content.

Repeated calls reset the timeout without replaying the entrance. Expiration removes styling; change metadata and navigation remain available. Caret changes, zoom, and scrolling do not replay existing highlights. Style updates apply immediately; entrance and exit fades reverse from their current opacity. Invalid numeric settings throw RangeError; invalid colors or animation settings throw TypeError.

Switching from all changes to recent changes fades out the excluded boxes. To replay an entrance deliberately, call clearHighlights({ animation: false }) before highlightChanges().

clearHighlights() uses the last requested exit duration and easing. Showing highlights during dismissal reverses the fade without adding duplicate boxes. snapshot().highlightsVisible becomes false when dismissal starts. Local edits, document replacement, and editor teardown remove stale highlights immediately.

Control the display time separately from the fade duration:

refresh.highlightChanges({
  timeoutMs: 5000,
  animation: { durationMs: 180 },
});

// Keep highlights until the user dismisses them.
refresh.highlightChanges({ timeoutMs: null });

The timeout starts after each highlightChanges() call. A timeout of 0 starts dismissal on the next timer task. An older timeout cannot clear highlights from a newer call. Edits, document replacement, explicit clearing, and editor teardown cancel the timer.

Disable motion for frequent updates or keyboard actions:

refresh.highlightChanges({ animation: false });
refresh.clearHighlights({ animation: false });

You can also choose a different dismissal duration:

refresh.clearHighlights({ animation: { durationMs: 125 } });

Reduced-motion preferences cap fades at 125 milliseconds. Switching that preference on finishes active fades immediately. Unsupported animation environments show or remove highlights immediately. Navigation stays immediate unless you request smooth scrolling. Reduced motion makes smooth navigation immediate.

For frequent server results, highlight only recent changes and keep previous changes available through explicit controls. Avoid repeatedly clearing and showing unchanged highlights. Keep a text count or change list beside the document; color alone does not explain what changed.

Add your own decoration

Use the typed options for the fill, border, spacing, and timing. Add a class for other CSS decoration:

refresh.highlightChanges({
  changeIds: ['delivery-date'],
  color: 'var(--review-fill)',
  borderColor: 'var(--review-border)',
  borderWidth: 2,
  borderStyle: 'dashed',
  borderRadius: 8,
  padding: 6,
  opacity: 0.2,
  className: 'server-change',
  animation: {
    durationMs: 180,
    exitDurationMs: 250,
    easing: 'cubic-bezier(0.23, 1, 0.32, 1)',
  },
});

Define your colors and decoration in application CSS:

.docx-editor {
  --review-fill: #93c5fd;
  --review-border: #2563eb;
  --review-shadow: #60a5fa;
}

.server-change {
  box-shadow: 0 0 10px var(--review-shadow);
}

The class decorates the overlay, not document text. Keep position, dimensions, pointer behavior, and animation under API control. The API controls fill opacity; the overlay opacity controls fades. The overlay remains decorative and does not enter saved DOCX files. Its default multiply blend keeps dark text readable. Your class can override mix-blend-mode. The default easing comes from --doc-motion-ease-out, with a built-in fallback. Set animation.easing to override it per call. This option does not accept CSS variables or inheritance keywords. Use animation: false to disable both fades.

Control change navigation

Import NavigateToChangeOptions from your adapter or @docx-editor.dev/core/editor. All navigation settings belong to the host API:

import type { NavigateToChangeOptions } from '@docx-editor.dev/react';

const navigation: NavigateToChangeOptions = {
  block: 'centerIfNeeded',
  behavior: 'instant',
  offsetPx: 24,
  focus: false,
};
refresh.navigateToChange('delivery-date', navigation);
OptionDefaultMeaning
block'center'Use 'start', 'center', 'centerIfNeeded', or 'nearest'.
behavior'instant'Use 'smooth' for an explicit animated navigation action.
offsetPx24Nonnegative edge padding for 'start' and 'nearest', in CSS pixels.
focusfalseMove focus and the caret to the changed range start.

centerIfNeeded keeps the viewport still when the target is already visible. Navigation returns whether the location is available; it does not await smooth scrolling. Use 'instant' when the highlight must start after the target is visible. Invalid navigation modes throw TypeError. Invalid padding throws RangeError. Keep automatic scrolling opt-in so server updates do not interrupt reading.

Handle a completely different document

A valid DOCX can contain unrelated content. applyUpdate() accepts it when the submission, sequence, and local revision checks pass. It replaces the whole document. It does not measure similarity or reject large changes.

Scroll preservation uses pixel offsets, not paragraph identity. A shorter replacement clamps the offset to its available scroll range. A longer replacement keeps the previous offset. Neither case guarantees that the reader sees the same subject.

The controller validates change locations against the returned file. It never reuses old locations for a replacement. Without metadata or tracked revisions, it reports unavailable change information. Incorrect metadata gives that change an invalid status; it does not refuse the whole file.

Use your application's document identity and job identity to prevent wrong-file responses. A submission token protects the open editor session. It does not prove which business document the server processed. Enforce this association on your backend, then verify its response before applyUpdate().

This example checks response headers from an application-defined endpoint:

const documentId = currentDocument.id;
const jobId = crypto.randomUUID();
const submission = await refresh.capture();

try {
  const response = await fetch('/api/update-document', {
    method: 'POST',
    headers: {
      'X-Document-Id': documentId,
      'X-Job-Id': jobId,
    },
    body: submission.bytes,
  });
  if (!response.ok) throw new Error('Processing failed');
  if (
    response.headers.get('X-Document-Id') !== documentId ||
    response.headers.get('X-Job-Id') !== jobId
  ) {
    throw new Error('Mismatched processor result');
  }

  const result = await refresh.applyUpdate({
    submission,
    sequence: 1,
    bytes: await response.arrayBuffer(),
  });
  if (!result.ok) showRefreshError(result.code);
} finally {
  refresh.finish(submission);
}

These headers form an application protocol, not editor API members or authentication. Correct identifiers cannot detect a server that attaches the wrong bytes. If your workflow requires content comparison, perform that comparison before accepting the result.

For an intentional switch to another business document, cancel processing and use your normal document-opening flow. An ordinary editor.load() invalidates pending refresh submissions.

Handle refused results

Use failure codes to choose the next action:

SituationResultApplication action
The user edits after capture, including an edit followed by undolocal-editsCapture the edited document and start a new job.
A duplicate result or an older accepted sequence arrivesout-of-orderDiscard that result.
A result belongs to an inactive submissionsupersededDiscard output for the previous submission.
The document changes during replacementdocument-changedDiscard output for the previous document.
Cancellation interrupts an active replacementcancelledStop the transport and discard later output.
Returned bytes are not a valid DOCXinvalid-documentKeep the visible document and report the processor failure.
A valid file fails during mountingload-failedReport the failure; inspect recovered for restoration success.
Mounting and restoration both failrecovery-failedOffer recovery and a recovery-file download.

Do not retry a refused result blindly. Cancellation does not reverse files that the controller already accepted. A response delivered after cancellation reports superseded. A document switch can report superseded, unavailable, or document-changed, depending on when the response arrives.

Connect your cancel control to both the transport and the controller:

const transport = new AbortController();
// Pass transport.signal to your fetch calls.
function cancelProcessing() {
  transport.abort();
  refresh.cancel();
}

Show status and handle recovery

subscribe() observes the cached snapshot(). Use it with React's useSyncExternalStore or a Vue shallowRef. onResult() reports every applyUpdate() completion, including refused results. Both subscriptions return an unsubscribe function.

The phases separate capture, processing, refresh, and recovery. Show translated status in a polite live region. Keep the document visible during processing and use a small refresh indicator. The standard document loading overlay stays hidden during external refresh. Highlight fades respect reduced-motion preferences. Navigation scrolls instantly by default.

The controller validates returned bytes before replacement. It retains recovery bytes until loading succeeds. If mounting fails, it reloads the previous document and reports load-failed. If recovery also fails, it reports recovery-failed. Offer recover() and a download of recoveryBytes() in that state. Recovery also resets selection and undo history. A document switch invalidates recovery, so it cannot overwrite a later document.

Handle the stable RefreshFailureCode values rather than error messages. capture() rejects with DocumentRefreshError when capture cannot proceed. Check error instanceof DocumentRefreshError, then read its typed code property. applyUpdate() returns an explicit success or failure result.

Offer recovery controls

Show these controls only when refresh.snapshot().recoveryAvailable is true. Keep recovery explicit after automatic restoration fails:

async function retryRecovery() {
  const recovered = await refresh.recover();
  if (!recovered) showRecoveryError();
}

function downloadRecovery() {
  const bytes = refresh.recoveryBytes();
  if (!bytes) return;

  const url = URL.createObjectURL(
    new Blob([bytes], {
      type: 'application/vnd.openxmlformats-officedocument.' + 'wordprocessingml.document',
    })
  );
  const link = document.createElement('a');
  link.href = url;
  link.download = 'document-recovery.docx';
  document.body.append(link);
  link.click();
  link.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

Recovery bytes contain the document from before the failed replacement. They are not an undo archive for successfully accepted files.

Run the example

Follow the setup in the DOCX document refresh example, then run bun run dev:refresh. The React client and mock server demonstrate scroll preservation, change navigation, temporary highlights, and refusal after local edits.

See also