Add Word-style commenting to documents with threaded discussions, replies, and resolution workflows.
Quick Start
const superdoc = new SuperDoc ({
selector: "#editor" ,
document: "contract.docx" ,
user: {
name: "John Smith" ,
email: "[email protected] " ,
},
modules: {
comments: {
readOnly: false ,
allowResolve: true ,
element: "#comments" ,
},
},
onCommentsUpdate : ({ type , comment }) => {
console . log ( "Comment event:" , type );
},
});
The comments module is enabled by default . To disable it entirely, set modules.comments to false: modules : {
comments : false ;
}
Module Configuration
View-only mode, prevents new comments
Allow marking comments as resolved
Container for comments sidebar
Enable dual internal/external comment system
Hide internal comments from view
Comments-only override for permission checks
During initialization
modules : {
comments : {
element : "#comments-sidebar" ;
}
}
After initialization
superdoc . on ( "ready" , () => {
const sidebar = document . querySelector ( "#comments-sidebar" );
superdoc . addCommentsList ( sidebar );
});
Fired for all comment changes.
Event type
pending - User selected text, about to add comment - add - New comment
created - update - Comment text edited - deleted - Comment removed -
resolved - Comment marked resolved - selected - Comment clicked/selected
onCommentsUpdate : ({ type , comment , meta }) => {
switch ( type ) {
case 'pending' :
showCommentDialog ();
break ;
case 'add' :
await saveComment ( comment );
break ;
case 'resolved' :
await markResolved ( comment . id );
break ;
}
}
Comment object Core properties Full JSON representation of comment content with ProseMirror schema {
"type" : "paragraph" ,
"attrs" : {
"lineHeight" : null ,
"textIndent" : null ,
"paraId" : null ,
"textId" : null ,
"rsidR" : null ,
"rsidRDefault" : null ,
"rsidP" : null ,
"rsidRPr" : null ,
"rsidDel" : null ,
"spacing" : {
"lineSpaceAfter" : 0 ,
"lineSpaceBefore" : 0 ,
"line" : 0 ,
"lineRule" : null
},
"extraAttrs" : {},
"marksAttrs" : null ,
"indent" : null ,
"borders" : null ,
"class" : null ,
"styleId" : null ,
"sdBlockId" : null ,
"attributes" : null ,
"filename" : null ,
"keepLines" : null ,
"keepNext" : null ,
"paragraphProperties" : null ,
"dropcap" : null ,
"pageBreakSource" : null ,
"justify" : null ,
"tabStops" : null
},
"content" : [
{
"type" : "text" ,
"text" : "Could you elaborate on this?"
}
]
}
Position Position in document Visual bounds (top, left, bottom, right) {
"top" : 96 ,
"left" : 201.9296875 ,
"right" : 241.9609375 ,
"bottom" : 19.5
}
Number of page comment belongs to
State Track changes ‘trackInsert’, ‘trackDelete’, or ‘trackFormat’
Text content of tracked changes
{
// Core properties
"commentId" : "a41eaa19-feb6-4e17-8bf3-d9fbf80e9f06" ,
"commentText" : "Could you elaborate on this?" ,
"parentCommentId" : null ,
"fileType" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ,
"mentions" : [],
"commentJSON" : { ... }, // see commentJSON under "Core properties" section
// Author information
"creatorName" : "Demo User" ,
"creatorEmail" : "[email protected] " ,
"createdTime" : 1762458019942 ,
// Position
"fileId" : "9c4ba8e0-6740-4531-ac8e-b2e8d30c2ffc" ,
"selection" : {
"selectionBounds" : {
"top" : 369.9765625 ,
"left" : 532.21875 ,
"right" : 661.7890625 ,
"bottom" : 404.875
},
"page" : 1 ,
"source" : "super-editor" ,
"documentId" : "9c4ba8e0-6740-4531-ac8e-b2e8d30c2ffc"
},
// State
"isInternal" : false ,
"resolvedTime" : null ,
"resolvedByEmail" : null ,
"resolvedByName" : null ,
// Track changes
"trackedChange" : null ,
"trackedChangeType" : null ,
"deletedText" : null ,
"trackedChangeText" : null ,
// Import metadata
"importedId" : null ,
"importedAuthor" : null
}
Threaded Replies
Comments support nested discussions:
// Parent comment
const parent = {
commentId: "parent-123" ,
commentText: "Should we change this?" ,
parentCommentId: null ,
};
// Reply
const reply = {
commentId: "reply-456" ,
commentText: "Yes, let's update" ,
parentCommentId: "parent-123" ,
};
// Nested reply
const nested = {
commentId: "reply-789" ,
commentText: "Done" ,
parentCommentId: "reply-456" ,
};
Resolution Workflow
onCommentsUpdate : ({ type , comment }) => {
if ( type === 'resolved' ) {
const resolution = {
commentId: comment . commentId ,
resolvedTime: comment . resolvedTime ,
resolvedByName: comment . resolvedByName ,
resolvedByEmail: comment . resolvedByEmail
};
await api . resolveComment ( resolution );
notifyParticipants ( comment );
}
}
Permission Control
const canResolve = ( comment , user , role ) => {
// Document owners can always resolve
if ( role === "editor" ) return true ;
// Authors can resolve their own
if ( comment . creatorEmail === user . email ) return true ;
return false ;
};
Word Import/Export
Word comments are automatically imported with the document and marked with
importedId
const importedComment = {
commentId: "uuid-generated" ,
importedId: "w15:123" , // Original Word ID
importedAuthor: {
name: "John Smith (imported)" ,
email: "[email protected] " ,
},
parentCommentId: "parent-uuid" , // Relationships preserved
};
Export mode
external - Include comments in DOCX - clean - Remove all comments -
Custom function for filtering
// With comments
const blob = await superdoc . export ({
commentsType: "external" ,
isFinalDoc: false ,
});
// Clean version
const cleanBlob = await superdoc . export ({
commentsType: "clean" ,
isFinalDoc: true ,
});
Permission Resolver
Customize who can resolve comments or accept tracked changes by returning false from a resolver function. The resolver receives the active permission (RESOLVE_OWN, RESOLVE_OTHER, REJECT_OWN, REJECT_OTHER), the current user, and any tracked-change metadata linked to the comment.
This controls the Resolve buttons in the comments panel and the Accept/Reject actions shown in tracked-change menus.
const superdoc = new SuperDoc ({
user: { name: "Alex Editor" , email: "[email protected] " },
modules: {
comments: {
permissionResolver : ({
permission ,
trackedChange ,
currentUser ,
defaultDecision ,
}) => {
const authorEmail = trackedChange ?. attrs ?. authorEmail ;
const activeUserEmail = currentUser ?. email ;
if (
permission === "RESOLVE_OTHER" &&
authorEmail &&
activeUserEmail !== authorEmail
) {
return false ; // Hide Accept button for suggestions from other authors
}
return defaultDecision ;
},
},
},
});
You can set a global resolver with the top-level permissionResolver config.
Module-level resolvers win when both are defined.
Track Changes Integration
Comments automatically generate for tracked changes:
Special comment for track changes ‘trackInsert’, ‘trackDelete’, or ‘trackFormat’
API Methods
Add comment to selected text.
superdoc . activeEditor . commands . insertComment ({
commentText: "Review this section" ,
creatorName: "John Smith" ,
creatorEmail: "[email protected] " ,
});
superdoc . activeEditor . commands . removeComment ({
commentId: "comment-123" ,
});
superdoc . activeEditor . commands . setActiveComment ({
commentId: "comment-123" ,
});
superdoc . activeEditor . commands . resolveComment ({
commentId: "comment-123" ,
});
Common Patterns
Contract Review Workflow
modules : {
comments : {
allowResolve : false , // Only owner resolves
element : '#legal-comments'
}
}
onCommentsUpdate : ({ type , comment }) => {
// Track progress
if ( type === 'add' ) stats . total ++ ;
if ( type === 'resolved' ) stats . resolved ++ ;
// Auto-assign
if ( comment . commentText . match ( / \b price | cost \b / i )) {
assignToFinanceTeam ( comment );
}
}
Internal Review System
modules : {
comments : {
useInternalExternalComments : true ;
}
}
onCommentsUpdate : ({ comment }) => {
if ( comment . isInternal ) {
syncToInternalTeam ( comment );
} else {
syncToAllUsers ( comment );
}
};
import { debounce } from "lodash" ;
const saveComment = debounce ( async ( comment ) => {
await api . updateComment ( comment . commentId , comment );
}, 1000 );
onCommentsUpdate : ({ type , comment }) => {
if ( type === "update" ) {
saveComment ( comment );
}
};
For documents with many comments: - Implement viewport-based loading -
Virtualize comment lists - Set reasonable limits (e.g., 500 comments max) -
Limit comment length (e.g., 10,000 characters)
const MAX_COMMENTS = 500 ;
onCommentsUpdate : ({ type }) => {
if ( type === "add" && commentCount >= MAX_COMMENTS ) {
showWarning ( "Maximum comments reached" );
return false ;
}
};