# Integrating comments with your application

The comments feature [provides an API](../../../api/comments.md) that lets you add, remove, and update comments in the editor. To save and access all these changes in your database, you first need to integrate this feature.

> **Note**
>
> This guide describes integrating comments as a standalone plugin (the asynchronous version of it). If you are using real-time collaboration, refer to the [Real-time collaboration features integration](../real-time-collaboration/real-time-collaboration-integration.md) guide.

<a id="integration-methods">

## Integration methods

This guide will discuss two ways to integrate CKEditor 5 with your comments data source:

* [A simple “load and save” integration](#a-simple-load-and-save-integration) using directly the `CommentsRepository` plugin API.
* [An adapter integration](#adapter-integration) which updates the comments data immediately in the database when it changes in the editor.

The adapter integration is the recommended one because it gives you better control over the data.

<a id="before-you-start">

## Before you start

> **Note**
>
> Complementary to this guide, we provide [ready-to-use **samples** available for download](https://github.com/ckeditor/ckeditor5-collaboration-samples/tree/master/collaboration-editor-classic). You may use the samples as an example or a starting point for your integration.

<a id="preparing-a-custom-editor-setup">

### Preparing a custom editor setup

To use the comments plugin, you need to prepare a custom editor setup with the asynchronous version of the comments feature included.

The easiest way to do that is by using the [Builder](https://builder.ckeditor.com/?redirect=docs). Pick a preset and start customizing your editor.

> **Note**
>
> The Builder allows you to pick your preferred distribution method and framework. For this guide, we will use the “Vanilla JS” option with “npm” and a simple setup based on the “Classic Editor (basic)” preset, with the comments feature enabled.

**In the “Features” section** of the Builder (2nd step), make sure to:

* turn off the “real-time” toggle next to the “Collaboration” group,
* enable the “Collaboration → Comments” feature.

Once you finish the setup, the Builder will provide you with the necessary HTML, CSS, and JavaScript code snippets. We will use those code snippets in the next step.

<a id="setting-up-a-sample-project">

### Setting up a sample project

Once we have a custom editor setup we need a simple JavaScript project to run it. For this, we recommend cloning the basic project template from our repository:

```bash
npx -y degit ckeditor/ckeditor5-tutorials-examples/sample-project sample-project
cd sample-project
npm install
```

Then, install the necessary dependencies:

```bash
npm install ckeditor5
npm install ckeditor5-premium-features
```

This project template uses [Vite](https://vitejs.dev/) under the hood and contains 3 source files that we will use: `index.html`, `style.css`, and `main.js`.

It is now the time to use our custom editor setup. **Go to the “Installation” section** of the Builder and copy the generated code snippets to those 3 files.

<a id="activating-the-feature">

### Activating the feature

To use this premium feature, you need to activate it with a license key. Refer to the [License key and activation](../../../getting-started/licensing/license-key-and-activation.md) guide for details.

After you have successfully obtained the license key open the `main.js` file and update the `your-license-key` string with your license key.

<a id="building-the-project">

### Building the project

Finally, build the project by running:

```bash
npm run dev
```

When you open the sample in the browser you should see the WYSIWYG editor with the comments plugin. However, it still does not load or save any data. You will learn how to add data to the comments plugin later in this guide.

Let’s now dive deeper into the structure of this setup.

<a id="basic-setups-anatomy">

### Basic setup’s anatomy

> **Note**
>
> Examples below implement the [wide sidebar display mode](../annotations/annotations-display-mode.md#wide-sidebar) for comment threads. If you want to use the inline display mode, remove parts of the snippets that set up the sidebar.

Let’s now go through the key fragments of this basic setup.

<a id="html-structure">

#### HTML structure

The HTML and CSS structure of the page creates two columns:

* `<div class="editor-container__editor">` is the container used by the editor.
* `<div class="editor-container__sidebar">` is the container used by the sidebar that holds the annotations (namely comments).

<a id="javascript">

#### JavaScript

The `main.js` file sets up the editor instance:

* Loads all necessary editor plugins (including the [`Comments`](../../../api/module_comments_comments-Comments.md) plugin).
* Sets the `licenseKey` configuration option.
* Sets the `sidebar.container` configuration option to the container mentioned above.
* Adds the `comment` and `commentsArchive` buttons to the editor toolbar.
* Defines the templates for the `CommentsIntegration` and `UsersIntegrations` plugins that we will use in the next steps of this tutorial.

> **Note**
>
> If you use the `ImageToolbar` plugin, also add the `comment` button to the [image toolbar](../../../api/module_image_imageconfig-ImageConfig.md#member-toolbar).

<a id="comments-api">

### Comments API

The integration below uses the comments API. Making yourself familiar with the API may help you understand the code snippets. In case of any problems, refer to the [comments API documentation](../../../api/comments.md).

<a id="next-steps">

### Next steps

We have set up a simple JavaScript project that runs a basic CKEditor 5 instance with the asynchronous version of the Comments feature. It does not yet handle loading or saving data, though. The next two sections cover the two available integration methods.

<a id="a-simple-load-and-save-integration">

## A simple “load and save” integration

In this solution, users and comments data is loaded during the editor initialization, and comments data is saved after you finish working with the editor (for example when you submit the form containing the WYSIWYG editor).

This method is recommended if you can trust your users or if you provide additional validation of the submitted data. This way, we can make sure that the user changes their comments only.

> **Note**
>
> Complementary to this guide, we provide [ready-to-use **samples** available for download](https://github.com/ckeditor/ckeditor5-collaboration-samples/tree/master/collaboration-editor-classic). You may use the samples as an example or as a starting point for your own integration.

<a id="loading-the-data">

### Loading the data

When the comments plugin is already included in the editor, you need to create a plugin which will initialize users and existing comments.

First, dump the users and comments data to a variable that will be available for your plugin.

> **Note**
>
> If your application needs to request the comments data from the server asynchronously, instead of putting the data in the HTML source, you can create a plugin that will fetch the data from the database. In this case, your plugin should [return a `Promise` from the `Plugin.init()` method](../../../api/module_core_plugin-PluginInterface.md#function-init) to make sure that the editor initialization waits for your data.

If you have set up the sample project as [recommended in the “Before you start” section](#before-you-start), open the `main.js` file and add this variable right after the imports:

```js
// Application data will be available under a global variable `appData`.
const appData = {
	// Users data.
	users: [
		{
			id: 'user-1',
			name: 'Mex Haddox'
		},
		{
			id: 'user-2',
			name: 'Zee Croce'
		}
	],

	// The ID of the current user.
	userId: 'user-1',

	// Comment threads data.
	commentThreads: [
		{
			threadId: 'thread-1',
			comments: [
				{
					commentId: 'comment-1',
					authorId: 'user-1',
					content: '<p>Are we sure we want to use a made-up disorder name?</p>',
					createdAt: new Date( '09/20/2018 14:21:53' ),
					attributes: {}
				},
				{
					commentId: 'comment-2',
					authorId: 'user-2',
					content: '<p>Why not?</p>',
					createdAt: new Date( '09/21/2018 08:17:01' ),
					attributes: {}
				}
			],
			context: {
				type: 'text',
				value: 'Bilingual Personality Disorder'
			},
			unlinkedAt: null,
			resolvedAt: null,
			resolvedBy: null,
			attributes: {}
		}
	],

	// Editor initial data.
	initialData:
		`<h2>
			<comment-start name="thread-1"></comment-start>
			Bilingual Personality Disorder
			<comment-end name="thread-1"></comment-end>
		</h2>
		<p>
			This may be the first time you hear about this made-up disorder but it actually isn’t so far from the truth.
			As recent studies show, the language you speak has more effects on you than you realize.
			According to the studies, the language a person speaks affects their cognition,
			behavior, emotions and hence <strong>their personality</strong>.
		</p>
		<p>
			This shouldn’t come as a surprise
			<a href="https://en.wikipedia.org/wiki/Lateralization_of_brain_function">since we already know</a>
			that different regions of the brain become more active depending on the activity.
			The structure, information and especially <strong>the culture</strong> of languages varies substantially
			and the language a person speaks is an essential element of daily life.
		</p>`
};
```

The Builder’s output sample already provides templates of two plugins: `UsersIntegration` and `CommentsIntegration`. Replace them with ones that read the data from `appData` and use the [`Users`](../../../api/module_collaboration-core_users-Users.md) and [`CommentsRepository`](../../../api/module_comments_comments_commentsrepository-CommentsRepository.md) API:

```js
class UsersIntegration extends Plugin {
	static get requires() {
		return [ 'Users' ];
	}

	static get pluginName() {
		return 'UsersIntegration';
	}

	init() {
		const usersPlugin = this.editor.plugins.get( 'Users' );

		// Load the users data.
		for ( const user of appData.users ) {
			usersPlugin.addUser( user );
		}

		// Set the current user.
		usersPlugin.defineMe( appData.userId );
	}
}

class CommentsIntegration extends Plugin {
	static get requires() {
		return [ 'CommentsRepository', 'UsersIntegration' ];
	}

	static get pluginName() {
		return 'CommentsIntegration';
	}

	init() {
		const commentsRepositoryPlugin = this.editor.plugins.get( 'CommentsRepository' );

		// Load the comment threads data.
		for ( const commentThread of appData.commentThreads ) {
			commentsRepositoryPlugin.addCommentThread( commentThread );
		}
	}
}
```

Update the `editorConfig.root.initialData` property to use the `appData.initialData` value:

```js
const editorConfig = {
	// ...

	root: {
		initialData: appData.initialData
	}

	// ...
};
```

And build the project:

```bash
npm run dev
```

You should now we see an editor instance with one comment thread.

<a id="saving-the-data">

### Saving the data

To save the comments data, you need to get it using the `CommentsRepository` API first. To do this, use the [`getCommentThreads()`](../../../api/module_comments_comments_commentsrepository-CommentsRepository.md#function-getCommentThreads) method.

Then, use the comment threads data to save it in your database in the way you prefer. See the example below.

In `index.html` add:

```html
<button id="get-data">Get data</button>
```

In `main.js` update the `ClassicEditor.create()` call with a chained `then()`:

```js
ClassicEditor
	.create( /* ... */ )
	.then( editor => {
		// After the editor is initialized, add an action to be performed after a button is clicked.
		const commentsRepository = editor.plugins.get( 'CommentsRepository' );

		// Get the data on demand.
		document.querySelector( '#get-data' ).addEventListener( 'click', () => {
			const editorData = editor.data.get();
			const commentThreadsData = commentsRepository.getCommentThreads( {
				skipNotAttached: true,
				skipEmpty: true,
				toJSON: true
			} );

			// Now, use `editorData` and `commentThreadsData` to save the data in your application.
			// For example, you can set them as values of hidden input fields.
			console.log( editorData );
			console.log( commentThreadsData );
		} );
	} );
```

> **Note**
>
> It is recommended to stringify the `attributes` value to JSON, save it as a string in your database, and then parse the value from JSON when loading comments.

<a id="demo">

### Demo

<!-- AI-AGENT-NOTE: An interactive demo is embedded here but is not represented in this Markdown file. If you need to see it in action, open this page in a browser (e.g. via a browser-automation MCP like Chrome DevTools or Playwright), or let the user know a live demo is available on this page. -->

<a id="data">

#### Data

<a id="comments">

#### Comments

<a id="adapter-integration">

## Adapter integration

Adapter integration uses an adapter object – provided by you – to immediately save changes in comments in your data store. It is the recommended way of integrating comments with your application because it lets you handle client-server communication more securely. For example, you can check user permissions, validate sent data, or update the data with information obtained on the server side, like the comment creation date. You will see how to handle the server response in the following steps.

> **Note**
>
> Complementary to this guide, we provide [ready-to-use **samples** available for download](https://github.com/ckeditor/ckeditor5-collaboration-samples/tree/master/collaboration-editor-classic). You may use the samples as an example or as a starting point for your own integration.

<a id="implementation">

### Implementation

First, define the adapter using the [`CommentsRepository#adapter`](../../../api/module_comments_comments_commentsrepository-CommentsRepository.md#member-adapter) property. [Adapter methods](../../../api/module_comments_comments_commentsrepository-CommentsAdapter.md) are called after the user makes a change in the comments. The adapter allows you to save the change in your database immediately. Each comment action has a separate adapter method that you should implement.

On the UI side, each change in comments is performed immediately, however, all adapter actions are asynchronous and are performed in the background. Because of this, all adapter methods need to return a `Promise`. When the promise is resolved, it means that everything went fine and a local change was successfully saved in the data store. When the promise is rejected, the editor throws a [`CKEditorError`](../../../api/module_utils_ckeditorerror-CKEditorError.md) error, which works nicely together with the [watchdog](../../watchdog.md) feature. When you handle the server response you can decide if the promise should be resolved or rejected.

While any adapter action is being performed, a pending action is automatically added to the editor [`PendingActions`](../../../api/module_core_pendingactions-PendingActions.md) plugin, so you do not have to worry that the editor will be destroyed before the adapter action has finished.

Now you are ready to implement the adapter.

If you have set up the sample project as [recommended in the “Before you start” section](#before-you-start), open the `main.js` file and add this variable right after the imports:

```js
// Application data will be available under a global variable `appData`.
const appData = {
	// Users data.
	users: [
		{
			id: 'user-1',
			name: 'Mex Haddox'
		},
		{
			id: 'user-2',
			name: 'Zee Croce'
		}
	],

	// The ID of the current user.
	userId: 'user-1',

	// Comment threads data.
	// Include all comment threads: active, resolved, and unlinked.
	commentThreads: [
		{
			threadId: 'thread-1',
			comments: [
				{
					commentId: 'comment-1',
					authorId: 'user-1',
					content: '<p>Are we sure we want to use a made-up disorder name?</p>',
					createdAt: new Date( '09/20/2018 14:21:53' ),
					attributes: {}
				},
				{
					commentId: 'comment-2',
					authorId: 'user-2',
					content: '<p>Why not?</p>',
					createdAt: new Date( '09/21/2018 08:17:01' ),
					attributes: {}
				}
			],
			context: {
				type: 'text',
				value: 'Bilingual Personality Disorder'
			},
			unlinkedAt: null,
			resolvedAt: null,
			resolvedBy: null,
			attributes: {}
		}
	],

	// Editor initial data.
	initialData:
		`<h2>
			<comment-start name="thread-1"></comment-start>
			Bilingual Personality Disorder
			<comment-end name="thread-1"></comment-end>
		</h2>
		<p>
			This may be the first time you hear about this made-up disorder but it actually isn’t so far from the truth.
			As recent studies show, the language you speak has more effects on you than you realize.
			According to the studies, the language a person speaks affects their cognition,
			behavior, emotions and hence <strong>their personality</strong>.
		</p>
		<p>
			This shouldn’t come as a surprise
			<a href="https://en.wikipedia.org/wiki/Lateralization_of_brain_function">since we already know</a>
			that different regions of the brain become more active depending on the activity.
			The structure, information and especially <strong>the culture</strong> of languages varies substantially
			and the language a person speaks is an essential element of daily life.
		</p>`

};
```

The Builder’s output sample already provides templates of two plugins `UsersIntegration` and `CommentsIntegration`. Replace them with ones that read the data from `appData` and use the [`Users`](../../../api/module_collaboration-core_users-Users.md) and [`CommentsRepository`](../../../api/module_comments_comments_commentsrepository-CommentsRepository.md) API:

```js
class UsersIntegration extends Plugin {
	static get requires() {
		return ['Users'];
	}

	static get pluginName() {
		return 'UsersIntegration';
	}

	init() {
		const usersPlugin = this.editor.plugins.get( 'Users' );

		// Load the users data.
		for ( const user of appData.users ) {
			usersPlugin.addUser( user );
		}

		// Set the current user.
		usersPlugin.defineMe( appData.userId );
	}
}

class CommentsIntegration extends Plugin {
	static get requires() {
		return [ 'CommentsRepository', 'UsersIntegration' ];
	}

	static get pluginName() {
		return 'CommentsIntegration';
	}

	init() {
		const commentsRepositoryPlugin = this.editor.plugins.get( 'CommentsRepository' );

		// Pre-load all comment threads (active, resolved, and unlinked) at initialization.
		// Use `isFromAdapter: true` to indicate this data comes from the backend
		// and prevent calling adapter.addCommentThread() for pre-loaded threads.
		for ( const commentThread of appData.commentThreads ) {
			commentsRepositoryPlugin.addCommentThread( { ...commentThread, isFromAdapter: true } );
		}

		// Set the adapter on the `CommentsRepository#adapter` property.
		commentsRepositoryPlugin.adapter = {
			addComment( data ) {
				console.log( 'Comment added', data );

				// Write a request to your database here. The returned `Promise`
				// should be resolved when the request has finished.
				// When the promise resolves with the comment data object, it
				// will update the editor comment using the provided data.
				return Promise.resolve( {
					createdAt: new Date()	   // Should be set on the server side.
				} );
			},

			updateComment( data ) {
				console.log( 'Comment updated', data );

				// Write a request to your database here. The returned `Promise`
				// should be resolved when the request has finished.
				return Promise.resolve();
			},

			removeComment( data ) {
				console.log( 'Comment removed', data );

				// Write a request to your database here. The returned `Promise`
				// should be resolved when the request has finished.
				return Promise.resolve();
			},

			addCommentThread( data ) {
				console.log( 'Comment thread added', data );

				// Write a request to your database here. The returned `Promise`
				// should be resolved when the request has finished.
				return Promise.resolve( {
					threadId: data.threadId,
					comments: data.comments.map( ( comment ) => ( { commentId: comment.commentId, createdAt: new Date() } ) ) // Should be set on the server side.
				} );
			},

			getCommentThread( data ) {
				console.log( 'Getting comment thread', data );

				// Write a request to your database here. The returned `Promise`
				// should resolve with the comment thread data.
				return Promise.resolve( {
					threadId: data.threadId,
					comments: [
						{
							commentId: 'comment-1',
							authorId: 'user-2',
							content: '<p>Are we sure we want to use a made-up disorder name?</p>',
							createdAt: new Date(),
							attributes: {}
						}
					],
					// It defines the value on which the comment has been created initially.
					// If it is empty it will be set based on the comment marker.
					context: {
						type: 'text',
						value: 'Bilingual Personality Disorder'
					},
					unlinkedAt: null,
					resolvedAt: null,
					resolvedBy: null,
					attributes: {},
					isFromAdapter: true
				} );
			},

			updateCommentThread( data ) {
				console.log( 'Comment thread updated', data );

				// Write a request to your database here. The returned `Promise`
				// should be resolved when the request has finished.
				return Promise.resolve();
			},

			resolveCommentThread( data ) {
				console.log( 'Comment thread resolved', data );

				// Write a request to your database here. The returned `Promise`
				// should be resolved when the request has finished.
				return Promise.resolve( {
					resolvedAt: new Date(), // Should be set on the server side.
					resolvedBy: usersPlugin.me.id // Should be set on the server side.
				} );
			},

			reopenCommentThread( data ) {
				console.log( 'Comment thread reopened', data );

				// Write a request to your database here. The returned `Promise`
				// should be resolved when the request has finished.
				return Promise.resolve();
			},

			removeCommentThread( data ) {
				console.log( 'Comment thread removed', data );

				// Write a request to your database here. The returned `Promise`
				// should be resolved when the request has finished.
				return Promise.resolve();
			}

		};
	}
}
```

> **Warning**
>
> Pre-load **all** comment threads (active, resolved, and unlinked) during editor initialization using `commentsRepository.addCommentThread()` with `isFromAdapter: true`. Do not rely on marker events alone — unlinked threads have no markers and will never be initialized that way.

The `getCommentThread()` adapter method is still required even when all threads are pre-loaded. It is called lazily by `CommentsRepository#fetchCommentThread()` for threads not loaded at init — for example, by [Revision History](../revision-history/revision-history.md) or when loading suggestion-linked comment threads on demand.

When handling `addSuggestion()` in your [Track Changes adapter](../track-changes/track-changes-integration.md#adapter-integration), also create a comment thread record in your backend using the suggestion ID. Comment threads for suggestions are not created through the adapter automatically.

Update the `editorConfig.root.initialData` property to use `appData.initialData` value:

```js
const editorConfig = {
	// ...

	root: {
		initialData: appData.initialData
	},

	// ...
};
```

And build the project:

```bash
npm run dev
```

You should now we see an editor instance with one comment thread. Observe the browser console while you interact with the comments feature in the editor (add/remove threads and comments).

> **Note**
>
> It is recommended to stringify the `attributes` value to JSON, save it as a string in your database, and then parse the value from JSON when loading comments.

<a id="demo-2">

### Demo

<!-- AI-AGENT-NOTE: An interactive demo is embedded here but is not represented in this Markdown file. If you need to see it in action, open this page in a browser (e.g. via a browser-automation MCP like Chrome DevTools or Playwright), or let the user know a live demo is available on this page. -->

Since the comments adapter saves the comment changes immediately after they are performed, it is also recommended to use the [Autosave](../../../api/module_autosave_autosave-Autosave.md) plugin to save the editor content after each change.

<a id="why-is-there-no-event-when-i-remove-comment-thread-markers-from-the-content">

### Why is there no event when I remove comment thread markers from the content?

Note that no remove event is fired when you remove the marker corresponding to the comment thread. Instead, the comment thread is resolved which triggers [`CommentsRepository#resolveCommentThread` event](../../../api/module_comments_comments_commentsrepository-CommentsRepository.md#event-resolveCommentThread). This operation can be restored using undo (`Cmd+Z` or `Ctrl+Z`), which will fire [`CommentsRepository#reopenCommentThread` event](../../../api/module_comments_comments_commentsrepository-CommentsRepository.md#event-reopenCommentThread).

However, you can still remove the comment thread by using available buttons in an annotation. Remember that the removal operation cannot be undone.

> **Note**
>
> When implementing adapter methods (`getCommentThread()` and `getComment()`), ensure they do not return deleted data, as removal is considered a permanent operation.

<a id="comments-samples">

## Comments samples

Please visit the [`ckeditor5-collaboration-samples`](https://github.com/ckeditor/ckeditor5-collaboration-samples/tree/master) GitHub repository to find several sample integrations of the comments feature.

---

Full index of the CKEditor 5 documentation: [llms.txt](../../../../llms.txt)
