Adding comments and replies in JavaScript PDF viewer
Nutrient Web SDK provides a user interface (UI) for viewing, adding, and deleting comments in PDF documents. Comments enable collaborative workflows where multiple users can discuss specific sections of a PDF document without leaving the viewer.
If you’re using Nutrient Web SDK with Document Engine and have Nutrient Instant enabled, comments are available starting from version 2020.1. For more information, refer to the Nutrient Instant guide.
If you’re not using Document Engine, comments are available starting from version 2023.3.
The main difference between the two setups is how collaboration works. With Document Engine, users see each other’s changes in real time. Without it, collaboration is asynchronous — changes appear only after a document is reloaded.
Licensing
Comments require a separate component in your Nutrient license. Without this included in your license, you won’t be able to add the functionality of viewing, searching, or adding comments in your application. Contact our Sales team to add comments to your license.
If you’re a new customer, you can try comments without a license key. If you’re an existing customer, ask our Sales team for a trial license if you’re interested.
Terminology
Before starting, here are a few key terms related to comments:
- Root annotation — The annotation to which all the comments in a single thread are linked.
- Comment thread — A group of comments associated with the same root annotation.
- Comment — A single comment added by a user.
Root annotation types
All comments are linked to their respective root annotations. The comments with the same root annotation are part of a single comment thread. There can be two types of root annotations:
MarkupAnnotation— You can start a new comment thread by selecting some text and clicking
in the markup annotation inline toolbar. In this case, the markup annotation created will act as a root annotation.CommentMarkerAnnotation— A comment marker annotation is a new annotation that can be added anywhere in a PDF document and used to start comment threads.
Getting started
By default, we don’t show the comment tool (
) in the main toolbar. This is because we want you to think about the workflow you want for your users and then decide whether or not you want to add it in the main toolbar. For example, if you want to enable the creation of comments from the main toolbar and disable sticky notes, use the following code snippet:
const toolbarItems = NutrientViewer.defaultToolbarItems .concat({ type: "comment" }) // Add comment tool. .filter((item) => item.type !== "note"); // Remove note tool.
NutrientViewer.load({ // ... toolbarItems,});You’ll have to add the comment tool in the main toolbar if you want to add comments using the comment marker annotation.
Adding a comment
You can add comments in two ways, depending on the type of root annotation.
From the main toolbar
This method involves the creation of CommentMarkerAnnotation before the creation of comments. To add a comment marker annotation, click the comment tool (
) in the main toolbar and choose a location on the page where you want to add the comment. A comment editor will appear where you can write your first comment and start a new thread.

Using markup annotations
To add a comment linked to a text annotation, create a new markup annotation and click the comment tool (
) in the inline toolbar. This opens a comment editor where you can write your first comment and begin a new thread.

Mentioning users in a comment
To mention a user in a comment, type @, followed by the user’s name, and select the user from the list.
Mentions are only supported in Nutrient Web SDK, and not on iOS or Android.
Setting the list of mentionable users
To specify the users who can be mentioned in comments, follow the steps below.
- Create a
MentionableUserobject for each mentionable user with the following string type properties:- Required:
name,id,displayName - Recommended:
description - Optional:
avatar
- Required:
- Create a list of the
MentionableUserobjects. - Pass the list to the
mentionableUsersconfiguration property when you load the Nutrient Web SDK viewer, or to thesetMentionableUsersmethod after loading the viewer.
The example below sets two mentionable users when loading the Nutrient Web SDK viewer:
NutrientViewer.load({ // ... Other configuration options. mentionableUsers: [ { name: "Jane Doe", displayName: "Jane Doe", id: "jane_doe", description: "jane@doe.com", }, { name: "John Doe", displayName: "John Doe", id: "john_doe", description: "john@doe.com", }, ],});The example below changes the list of mentionable users after the Nutrient Web SDK viewer has loaded:
instance.setMentionableUsers([ { name: "Jane Doe", displayName: "Jane Doe", id: "jane_doe", description: "jane@doe.com", }, { name: "John Doe", displayName: "John Doe", id: "john_doe", description: "john@doe.com", },]);Getting the list of users mentioned in a comment
To get the list of all users mentioned in a comment, call the getMentionedUserIds method on the comment object:
comment.getMentionedUserIds();Notifying mentioned users
To notify users who are mentioned in a comment, add event listeners to the loaded instance.
One approach is to listen to the changes made by a specific user. To do this, listen to the comments.mention event and send notifications when the event is triggered. In the example below, the listener is triggered when the user john_doe adds or removes the someCommentObject comment. The listener won’t trigger if the changes are made by another user, even if those changes are visible to you:
instance.addEventListener("comments.mention", args: { comment: someCommentObject, modifications: [{ userId: "john_doe", action: "ADDED" | "REMOVED" }] } => void)Another approach is to listen to all changes to comments irrespective of who made them. Listen to the comments.create, comments.update, and comments.delete events to send notifications when comments are created, updated, or deleted. The example below adds separate event listeners for each of these three cases and determines the users mentioned in the affected comments. If you implement a way to keep track of users mentioned in different comments, you can determine if new user mentions have been added or deleted and send notifications to the affected users. This approach gives you control over the different ways comments can change (creation, update, deletion). The limitation of this approach is that changes by any user will trigger notifications; you cannot specify the users whose changes will trigger notifications. Using this approach, you can’t determine who made the last change to a comment, but you can use the comment.creatorName property to determine the user who created the comment:
instance.addEventListener("comments.create", (createdComments) => { const users = createdComments .get(0) .forEach((comment) => comment.getMentionedUserIds());});
instance.addEventListener("comments.update", (updatedComments) => { const users = updatedComments .get(0) .forEach((comment) => comment.getMentionedUserIds());});
instance.addEventListener("comments.delete", (deletedComments) => { const users = deletedComments .get(0) .forEach((comment) => comment.getMentionedUserIds());});Deleting a comment
You can delete an individual comment by clicking the (
) delete button. If all the comments of a thread are deleted, the corresponding root annotation is automatically deleted.
Document Engine currently doesn’t expose any APIs for deleting a comment.
Working with comments programmatically
While the UI provides tools for users to add and manage comments interactively, you can also create, read, update, and delete (CRUD) comments programmatically using the Nutrient Web SDK API. This is useful for:
- Automating comment workflows
- Importing comments from external systems
- Building custom commenting interfaces
- Pre-populating documents with review comments
Basic workflow
Comments consist of two parts that must be created together:
CommentMarkerAnnotation— The visual marker on the pageComment— The actual comment content
Comments are immutable data structures. To modify a comment, use the .set() method to create a new instance with updated properties, similar to how ViewState works.
Playground example
Here’s a complete Playground example(opens in a new tab) showing all CRUD operations:
NutrientViewer.load({ ...baseOptions, theme: NutrientViewer.Theme.DARK,}).then(async (instance) => { const id = NutrientViewer.generateInstantId();
// 1. CREATE: Start a comment thread. const markerAnnotation = new NutrientViewer.Annotations.CommentMarkerAnnotation({ id, pageIndex: 0, boundingBox: new NutrientViewer.Geometry.Rect({ top: 50, left: 50, width: 20, height: 20, }), });
const comment = new NutrientViewer.Comment({ pageIndex: 0, text: { format: "plain", value: "Hello world comment", }, rootId: id, });
await instance.create([markerAnnotation, comment]);
// 2. READ: Retrieve all comments. let comments = await instance.getComments(); const commentToEdit = comments.find((comment) => comment.text.value.includes("Hello world"), );
// 3. UPDATE: Modify the comment. const updatedComment = commentToEdit.set("text", { format: "plain", value: "Hi there", }); await instance.update(updatedComment);
// 4. DELETE: Remove the comment (optional). // await instance.delete(commentToEdit.id);});Creating comments
To create a comment thread programmatically, use this code:
const id = NutrientViewer.generateInstantId();
// Create the marker annotation.const marker = new NutrientViewer.Annotations.CommentMarkerAnnotation({ id, pageIndex: 0, boundingBox: new NutrientViewer.Geometry.Rect({ top: 50, left: 50, width: 20, height: 20, }),});
// Create the comment.const comment = new NutrientViewer.Comment({ pageIndex: 0, text: { format: "plain", value: "Your comment text" }, rootId: id, // Links comment to marker});
// Add both to the document.await instance.create([marker, comment]);Key points:
- Use
NutrientViewer.generateInstantId()to create a unique ID. - The
rootIdproperty links the comment to its marker. - Both entities share the same
id/rootIdvalue. - Use
instance.create()with an array containing both objects.
Reading comments
Retrieve all saved comments using instance.getComments():
const allComments = await instance.getComments();console.log(`Total comments: ${allComments.size}`);
// Find a specific comment.const myComment = allComments.find((c) => c.rootId === id);
// Find comments by text content.const searchResults = allComments.filter((c) => c.text.value.includes("search term"),);Only saved/persisted comments are returned. Active drafts being edited in the UI aren’t accessible via this API.
Updating comments
Modify existing comments using instance.update():
const comments = await instance.getComments();const target = comments.find((c) => c.text.value.includes("Hello world"));
if (target) { // Update single or multiple properties using `.set()`. const updated = target .set("text", { format: "plain", value: "Updated text" }) .set("creatorName", "Updated Author");
await instance.update(updated);}Deleting comments
Remove comments programmatically using instance.delete():
// Delete by comment ID.await instance.delete(commentToEdit.id);
// Or delete the marker annotation (removes all associated comments).await instance.delete(markerAnnotation.id);
// Delete multiple comments at once.await instance.delete([comment1.id, comment2.id, comment3.id]);Comments are automatically removed when their root annotation is deleted. If you use instance.history.undo() to restore a deleted root annotation, its associated comments will also be restored.
Limitations
The programmatic API works with saved comments only. Active comment drafts being edited in the UI cannot be modified via API, as the comment editor manages its own internal state.
If you need to set initial comment text programmatically, use setOnCommentCreationStart():
instance.setOnCommentCreationStart((comment) => { return comment.set("text", { format: "plain", value: "Pre-populated text", });});For use cases requiring live draft editing — such as accessibility tools like dictation or text prediction — contact our Support team to discuss your requirements.
API reference
For detailed API documentation, see:
CommentClass — Comment object properties and methodsinstance.create()— Create comments programmaticallyinstance.getComments()— Retrieve all commentsinstance.update()— Update saved commentsinstance.delete()— Delete commentsCommentMarkerAnnotation— Visual marker for comment threads
Disabling the comments UI
If your license includes comments but you want to disable letting the user view and add comments, you can set showComments to false in ViewState:
const initialViewState = new NutrientViewer.ViewState({ showComments: false,});
const instance = NutrientViewer.load({ // ... other options initialViewState: initialViewState,});Comment permissions
There might be situations where you want to disable the creation or deletion of individual comments based on some condition. To do this, you can define the isEditableComment function as a configuration option when initializing the viewer. When the return value of the isEditableComment method is false for a comment, the comment can no longer be deleted by the user. Similarly, isEditableComment can be used to determine whether or not a user can reply to existing threads.
In the example below, all the comments that have a root annotation with an id other than the rootAnnotationId will be editable:
NutrientViewer.load({ // ... isEditableComment: (comment) => { return comment.rootId !== rootAnnotationId; },});For every comment thread, isEditableComment receives a temporary draft comment with pageIndex=null. The rootId of this draft comment points to the root annotation of the comment thread. If isEditableComment returns false, the user won’t be able to add comments in that comment thread.
In the example below, a user can’t add a comment in any comment thread:
NutrientViewer.load({ // ... isEditableComment: (comment) => { return comment.pageIndex !== null; },});To set the permissions after a viewer instance has been created, you can use instance.setIsEditableComment:
NutrientViewer.load(options).then((instance) => { instance.setIsEditableComment((comment) => { return comment.rootId !== rootAnnotationId; });});Customizing a comment block
Customize the look of a comment block using CSS. Make sure you’re using the public class names starting with PSPDFKit-, as other class names might change in the future and break your application.
For example, if you want to customize the border-radius of avatars, you can do that by writing the following CSS:
.PSPDFKit-Comment-Avatar { border-radius: 6px;}You can also show avatars in comment blocks by setting a custom renderer:
NutrientViewer.load({ customRenderers: { CommentAvatar: (comment: Comment) => ({ node: element, append: false, // This should always be `false` in this case. }), },});If you want to change the avatar after the viewer instance has been created, you can use setCustomRenderers.
Responsive UI
The comments UI adapts to the screen size of your browser: When there’s enough space in the viewport outside of the current page, comment threads are displayed in a sidebar on the side of the page and are always visible. However, when the remaining viewport space gets too small for floating comments threads to be visible, they’re concealed until the user clicks or taps on a root annotation.
This default behavior can be customized by modifying the value of ViewState#commentDisplay, which accepts the following values:
NutrientViewer.CommentDisplay.FITTING— The default value; will show comments in a floating sidebar by the page side when there’s enough space to do so, and in a popover when there isn’t, and the marker annotation is selected.NutrientViewer.CommentDisplay.FLOATING— Will always show comments in a floating sidebar by the page side, except whenViewState#zoomis set toNutrientViewer.ZoomeMode#FIT_TO_WIDTH, in which case, they’re displayed in a popover dialog instead.NutrientViewer.CommentDisplay.POPOVER— Will always show comments in a popover when the marker annotation is selected.
Note that the setting above only affects desktop and tablet screens; in mobile devices, comments are always displayed in a drawer at the bottom of the viewport.
Here’s an example of how to change it:
NutrientViewer.load({ ...config, initialViewState: new NutrientViewer.ViewState({ commentDisplay: NutrientViewer.CommentDisplay.FLOATING, }),});Rich text comments
Rich text editing is supported in comments. Using the UI, you can select parts of a comment and do the following:
- Make parts of the comment bold, italic, or underlined.
- Change the color and the background color of the parts of the comment.
- Add hyperlinks to the comment.