Comments

Read, create, reply to, and resolve DOCX comment threads. Use the packaged review rail or build an interface with useReview.

Comments attach a discussion to a text range.

The editor reads existing OOXML comments during load. It shows them beside the page and writes them during save.

You can continue threads created in Microsoft Word. Word can also continue threads created in this editor.

Comments share one sidebar, hook, and card layout with tracked changes.

Comments require the review module from @docx-editor.dev/pro.

Prerequisites

TaskRequirement
Read commentsA loaded document
Show the review railreviewModule() and DocxEditorReview
Create or reply in a browserA non-empty author, reviewModule(), an editable mode, and an attached editor
Resolve, reopen, or delete in browserreviewModule(), an editable mode, and an attached editor
Create or reply on a serverA non-empty author and a server runtime

OOXML requires w:author for each comment and reply. The engine refuses a write without an author.

For module registration and licensing, see the Pro package documentation.

Write a comment

The review hook's comment(text, author?) comments on the current selection.

It returns whether the editor applied the write. Keep the draft text when the method returns false.

This example keeps the draft after a refused write:

import { useState } from 'react';
import { useReview } from '@docx-editor.dev/pro/react';

function CommentBox() {
  const { comment, selectionAnchorY } = useReview();
  const [text, setText] = useState('');

  // null when nothing is selected: there is nowhere to anchor a comment.
  if (selectionAnchorY === null) return null;

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        if (comment(text)) setText('');
      }}
    >
      <textarea value={text} onChange={(e) => setText(e.target.value)} />
      <button type="submit" disabled={!text.trim()}>
        Comment
      </button>
    </form>
  );
}

selectionAnchorY is the proposed comment's document-space Y coordinate.

The engine calculates it without the DOM, as it does for card anchors. Use it to position a custom compose box beside the selection.

The packaged DocxEditorReview.AddComment and DocxEditorReview.Draft parts provide this workflow in Vue and React.

Create a comment with editor-api

The Pro-licensed Office-shaped API creates the same canonical comment from an explicit range.

This example comments on the first search result:

const matches = context.document.body.search('payment terms');
matches.load('items');
await context.sync();

const comment = matches.items[0].insertComment('Confirm this with Legal.');
await context.sync();

Create the runtime with { author: 'Jess Lin' }.

The write uses one package transaction and one browser Undo unit. Collapsed ranges create insertion-point comments.

The editor refuses cross-cell anchors and empty comment text. It does not change them to approximate values.

Reply to a thread

reply(item, text, author?) adds a reply to a comment.

You can also reply to a revision. The editor creates a comment over the revision range because OOXML gives w:ins and w:del no body.

This example adds a fixed reply to any review item:

function Thread() {
  const { items, reply } = useReview();

  return (
    <ul>
      {items.map((item) => (
        <li key={item.key}>
          <p>{item.text}</p>
          <span>
            {item.author}
            {item.date ? ` · ${item.date}` : ''}
          </span>
          <span>{item.replyIds.length} replies</span>
          <button onClick={() => reply(item, 'Agreed, rephrased.')}>Reply</button>
        </li>
      ))}
    </ul>
  );
}

Like comment, reply returns whether the editor applied the write. It does not throw for a refused write.

Read threads

useReview().items contains comments and revisions. Filter by kind when you need one item type.

This example narrows the item type to comments:

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

function CommentsOnly() {
  const { items } = useReview();
  const comments = items.filter(
    (item): item is Extract<ReviewItemView, { kind: 'comment' }> => item.kind === 'comment'
  );

  return (
    <ul>
      {comments.map((c) => (
        <li key={c.key} data-resolved={c.resolved || undefined}>
          {/* File-derived. Render as text, never as markup. */}
          {c.text}: {c.author}
        </li>
      ))}
    </ul>
  );
}

Comment items include these fields:

FieldMeaning
key, idStable review and OOXML identifiers
author, initials, dateComment author metadata
textFile-derived comment text
replyIdsReply identifiers in the thread
resolvedWhether w15:commentsEx marks the thread complete
parentIdParent comment identifier; absent on a thread root
anchorY, pageIndexDocument placement
isActiveWhether the caret is in the item

The editor reads initials from w:initials when available. Otherwise, it derives initials from the author name.

For a document-level read without the review module, editor.getComments() returns { id, text, resolved } for each thread.

Resolve and reopen threads

The packaged card renders <DocxEditorReview.Resolve /> on an open comment and <DocxEditorReview.Reopen /> on a resolved one. Custom cards use the matching hook actions:

ActionResultRefusal behavior
comment(text, author?)Adds a thread to the selectionReturns false; the caller keeps its draft
reply(item, text, author?)Adds a reply, or comments on a revision rangeReturns false
resolve(item)Marks an open thread completeRepeating it succeeds without a write
reopen(item)Reopens a complete threadRepeating it succeeds without a write
remove(item)Deletes a comment thread or rejects a tracked changeReturns false when refused

The hook does not own the caller's draft state.

commentResolutionDisabledReason explains why Resolve and Reopen are unavailable.

This example selects the correct action for the comment state:

function CommentDecision({ item }: { item: ReviewItemView }) {
  const { resolve, reopen, commentResolutionDisabledReason } = useReview();
  if (item.kind !== 'comment') return null;

  return (
    <button
      disabled={commentResolutionDisabledReason !== null}
      title={commentResolutionDisabledReason ?? undefined}
      onClick={() => (item.resolved ? reopen(item) : resolve(item))}
    >
      {item.resolved ? 'Reopen' : 'Resolve'}
    </button>
  );
}

Both actions return whether the editor accepted the request. A stale item or non-comment item returns false.

Resolve on a resolved thread succeeds without changing the document. Reopen on an open thread behaves the same way.

These no-op actions do not create an Undo entry. Resolving writes the complete thread in one transaction.

One Undo action restores the prior state. Saving and reopening preserves the resolved state.

Viewing mode disables both actions. commentResolutionDisabledReason contains the engine refusal the document is open for viewing.

Use this value to explain why a custom control is disabled.

Editing and suggesting modes allow thread-state changes. Resolution changes metadata, not tracked document content.

Listen for comment changes

Writing a comment or reply emits the editor's change event.

Use this event for autosave and dirty tracking:

useEditorEvent('change', (change) => void autosave(change.revision));

Work with Word

The editor reads and writes author, initials, date, reply threading, and resolved state in the document's comments part.

A document annotated in this editor opens in Word with the same threads. Resolved threads remain resolved.

The editor reads and writes comments in headers, footers, footnotes, and endnotes within their own scopes.

Next steps

On this page