Create and resolve comment threads

Anchor a DOCX comment to document content, add a reply, and resolve the thread.

Comments are document content. Use the Document API when code needs to create, inspect, update, or delete a thread without driving the built-in comments interface.

This guide creates one anchored thread, adds a reply, and resolves it. The operations work against an open document in the Editor and through supported headless clients.

Run the complete workflow

Copy the tracked-changes fixture to your app's public directory as contract.docx, then run this browser example:

import { SuperDoc } from 'superdoc';
import 'superdoc/style.css';

const superdoc = new SuperDoc({
  selector: '#editor',
  document: '/contract.docx',
  onReady: async ({ superdoc }) => {
    const doc = superdoc.activeEditor?.doc;
    if (!doc) throw new Error('The active document is unavailable.');

    const match = await doc.query.match({
      select: {
        type: 'text',
        pattern: 'Confidential Information',
      },
      require: 'exactlyOne',
    });
    const clause = match.items[0];

    if (!clause || clause.matchKind !== 'text') {
      throw new Error('The clause was not found.');
    }

    const createReceipt = await doc.comments.create(
      {
        target: clause.target,
        text: 'Confirm that this definition matches the current policy.',
      },
      { expectedRevision: match.evaluatedRevision },
    );

    if (!createReceipt.success) {
      throw new Error(`Comment creation failed: ${createReceipt.failure.message}`);
    }

    const afterCreate = await doc.comments.list({ includeResolved: true });
    const replyReceipt = await doc.comments.create(
      {
        parentCommentId: createReceipt.id,
        text: 'Confirmed against the policy dated July 2026.',
      },
      { expectedRevision: afterCreate.evaluatedRevision },
    );

    if (!replyReceipt.success) {
      throw new Error(`Reply failed: ${replyReceipt.failure.message}`);
    }

    const afterReply = await doc.comments.list({ includeResolved: true });
    const resolveReceipt = await doc.comments.patch(
      {
        commentId: createReceipt.id,
        status: 'resolved',
      },
      { expectedRevision: afterReply.evaluatedRevision },
    );

    if (!resolveReceipt.success) {
      throw new Error(`Resolve failed: ${resolveReceipt.failure.message}`);
    }

    console.log('Resolved comment:', createReceipt.id);
  },
});

window.addEventListener('beforeunload', () => superdoc.destroy());

The example deliberately re-lists comments between mutations. Each mutation advances the document revision, so the next operation uses a current expectedRevision.

Anchor the root comment

A root comment needs text and a document target. A text query returns a SelectionTarget that can be passed directly to comments.create().

Do not derive the target from the rendered DOM or from highlightRange. DOM positions belong to layout. highlightRange describes the displayed snippet. The query target addresses document content.

On success, the creation receipt includes the new thread ID. Keep that ID for replies and later lifecycle changes.

Add replies without another target

A reply belongs to an existing thread. Pass parentCommentId and text. Do not pass the root target again.

The reply is another document mutation. Inspect its receipt before resolving the thread or saving the file.

Resolve or reopen the thread

comments.patch() changes exactly one comment field per call. Set status: 'resolved' to resolve the root thread. Set status: 'active' in a later call to reopen it.

List with includeResolved: true when the application needs to show both states. A resolved thread remains in the DOCX unless it is explicitly deleted.

Use comments in the built-in UI for the standard human workflow. Use a custom comments UI when your application owns the thread list and navigation.

On this page