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 );
}
});
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 ;
}
}