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
| Task | Requirement |
|---|---|
| Read comments | A loaded document |
| Show the review rail | reviewModule() and DocxEditorReview |
| Create or reply in a browser | A non-empty author, reviewModule(), an editable mode, and an attached editor |
| Resolve, reopen, or delete in browser | reviewModule(), an editable mode, and an attached editor |
| Create or reply on a server | A 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>
);
}<script setup lang="ts">
import { ref } from 'vue';
import { useReview } from '@docx-editor.dev/pro/vue';
const text = ref('');
const review = useReview();
function submit() {
if (review.comment(text.value)) {
text.value = '';
}
}
</script>
<template>
<form v-if="review.selectionAnchorY.value !== null" @submit.prevent="submit">
<textarea v-model="text" />
<button type="submit" :disabled="!text.trim()">Comment</button>
</form>
</template>Mount this component inside DocxEditorRoot.
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>
);
}<script setup lang="ts">
import { useReview } from '@docx-editor.dev/pro/vue';
const review = useReview();
</script>
<template>
<ul>
<li v-for="item in review.items.value" :key="item.key">
<p>{{ item.text }}</p>
<span>{{ item.author }}</span>
<span>{{ item.replyIds.length }} replies</span>
<button type="button" @click="review.reply(item, 'Agreed, rephrased.')">Reply</button>
</li>
</ul>
</template>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>
);
}<script setup lang="ts">
import { computed } from 'vue';
import { useReview } from '@docx-editor.dev/pro/vue';
const review = useReview();
const comments = computed(() => review.items.value.filter((item) => item.kind === 'comment'));
</script>
<template>
<ul>
<li v-for="comment in comments" :key="comment.key">{{ comment.text }}: {{ comment.author }}</li>
</ul>
</template>Render comment text with interpolation. The text comes from the file.
Comment items include these fields:
| Field | Meaning |
|---|---|
key, id | Stable review and OOXML identifiers |
author, initials, date | Comment author metadata |
text | File-derived comment text |
replyIds | Reply identifiers in the thread |
resolved | Whether w15:commentsEx marks the thread complete |
parentId | Parent comment identifier; absent on a thread root |
anchorY, pageIndex | Document placement |
isActive | Whether 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:
| Action | Result | Refusal behavior |
|---|---|---|
comment(text, author?) | Adds a thread to the selection | Returns false; the caller keeps its draft |
reply(item, text, author?) | Adds a reply, or comments on a revision range | Returns false |
resolve(item) | Marks an open thread complete | Repeating it succeeds without a write |
reopen(item) | Reopens a complete thread | Repeating it succeeds without a write |
remove(item) | Deletes a comment thread or rejects a tracked change | Returns 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>
);
}<script setup lang="ts">
import { useReview, type ReviewItemView } from '@docx-editor.dev/pro/vue';
defineProps<{ item: ReviewItemView }>();
const review = useReview();
</script>
<template>
<button
v-if="item.kind === 'comment'"
type="button"
:disabled="review.commentResolutionDisabledReason.value !== null"
:title="review.commentResolutionDisabledReason.value ?? undefined"
@click="item.resolved ? review.reopen(item) : review.resolve(item)"
>
{{ item.resolved ? 'Reopen' : 'Resolve' }}
</button>
</template>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
- Tracked changes: Use the full
useReviewAPI, sidebar parts, and filters. - Review colors and styling: Color comment authors and restyle the cards.
- Vue composition: Arrange Vue editor and review parts.
- React composition: Arrange React editor and review parts.
- Custom nodes: Add custom-node chrome.