> ## Documentation Index
> Fetch the complete documentation index at: https://velt.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# CKEditor 5 Editor

> Setup Multiplayer Editing for CKEditor 5.

The `@veltdev/ckeditor-crdt-react` and `@veltdev/ckeditor-crdt` libraries enable real-time collaborative editing in CKEditor 5. The collaboration engine is built on [Yjs](https://docs.yjs.dev/) and the Velt SDK.

## Prerequisites

* Node.js (v14 or higher)
* A Velt account with an API key ([sign up](https://console.velt.dev/))
* React (v18 or higher) when using the React wrapper
* A CKEditor 5 license compatible with your build
* Optional: TypeScript for type safety

## Setup

### Step 1: Install Dependencies

> [tab] React / Next.js
>
> ```bash theme={null}
> npm install @veltdev/ckeditor-crdt-react @veltdev/ckeditor-crdt @veltdev/react @veltdev/types ckeditor5 @ckeditor/ckeditor5-react yjs y-protocols
> ```

> [tab] Other Frameworks
>
> ```bash theme={null}
> npm install @veltdev/ckeditor-crdt @veltdev/crdt @veltdev/client ckeditor5 yjs y-protocols
> ```

The React package provides the drop-in component and hook. The base package provides the imperative manager, provider adapter, and HTML content helpers.

```ts theme={null}
// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  resolve: {
    dedupe: ['ckeditor5', 'yjs', 'y-protocols', 'lib0'],
  },
  optimizeDeps: {
    include: ['ckeditor5'],
  },
});
```

### Step 2: Setup Velt

Initialize Velt, authenticate a user, and set a stable document context before collaboration starts. Follow the [Velt Setup Docs](https://velt.dev/docs/get-started/quickstart.md) for the production token endpoint used by `authProvider`.

> [tab] React / Next.js
>
> ```tsx theme={null}
> import { useEffect, useState } from 'react';
> import {
>   VeltProvider,
>   useCurrentUser,
>   useSetDocuments,
> } from '@veltdev/react';
>
> const DOCUMENT_ID = 'article-123';
> const USER = {
>   userId: 'user-1',
>   name: 'Ada Lovelace',
>   email: 'ada@example.com',
>   organizationId: 'org-1',
> };
>
> const authProvider = {
>   user: USER,
>   retryConfig: { retryCount: 3, retryDelay: 1000 },
>   generateToken: async () => {
>     const response = await fetch('/api/velt/token', {
>       method: 'POST',
>       headers: { 'Content-Type': 'application/json' },
>       body: JSON.stringify(USER),
>     });
>     const { token } = await response.json();
>     return token;
>   },
> };
>
> function CollaborationScope() {
>   const user = useCurrentUser();
>   const { setDocuments } = useSetDocuments();
>   const [documentReady, setDocumentReady] = useState(false);
>
>   useEffect(() => {
>     if (!user) {
>       setDocumentReady(false);
>       return;
>     }
>     setDocuments([{
>       id: DOCUMENT_ID,
>       metadata: { documentName: 'Article 123' },
>     }]);
>     setDocumentReady(true);
>   }, [user, setDocuments]);
>
>   return user && documentReady
>     ? <CollaborativeEditor />
>     : <p>Preparing collaboration...</p>;
> }
>
> <VeltProvider apiKey="YOUR_API_KEY" authProvider={authProvider}>
>   <CollaborationScope />
> </VeltProvider>
> ```

> [tab] Other Frameworks
>
> Authenticate before setting the document. Wait for Velt readiness before creating CKEditor:
>
> ```ts theme={null}
> import { initVelt } from '@veltdev/client';
>
> const DOCUMENT_ID = 'article-123';
> const client = await initVelt('YOUR_API_KEY');
>
> await client.identify({
>   userId: 'user-1',
>   name: 'Ada Lovelace',
>   email: 'ada@example.com',
>   organizationId: 'org-1',
> });
>
> await client.setDocument(DOCUMENT_ID, {
>   documentName: 'Article 123',
> });
>
> const initSubscription = client.getVeltInitState().subscribe((isReady) => {
>   if (isReady) void initializeCKEditor(client);
> });
> ```

### Step 3: Configure CKEditor

Import the editor constructor, the features your build needs, and CKEditor styles:

```tsx theme={null}
import {
  ClassicEditor,
  Essentials,
  Paragraph,
  Bold,
  Italic,
  Underline,
  Heading,
  List,
  Highlight,
  FontColor,
  FontBackgroundColor,
  Undo,
  GeneralHtmlSupport,
} from 'ckeditor5';
import 'ckeditor5/ckeditor5.css';

const editorConfig = {
  licenseKey: 'GPL',
  plugins: [
    Essentials,
    Paragraph,
    Bold,
    Italic,
    Underline,
    Heading,
    List,
    Highlight,
    FontColor,
    FontBackgroundColor,
    Undo,
    GeneralHtmlSupport,
  ],
  toolbar: [
    'undo', 'redo', '|',
    'heading', '|',
    'bold', 'italic', 'underline', '|',
    'fontColor', 'fontBackgroundColor', 'highlight', '|',
    'bulletedList', 'numberedList',
  ],
};
```

Use your commercial CKEditor license key instead of `GPL` when required by your distribution.

CKEditor serializes highlights as `<mark>` elements. This markup persists through realtime sync, force reset, and version restore:

```html theme={null}
<mark class="marker-yellow" style="background-color:#fff59d;">Highlighted</mark>
```

### Step 4: Initialize Collaborative Editor

* Pass an `editorId` to uniquely identify the shared editor.
* Do not pass CKEditor React's `data` or `onReady` to the drop-in component; the CRDT wrapper owns content and editor capture.
* Use `initialContent` as HTML seed content for a new shared document.

The wrapper registers its Yjs fragment observer before the Store loads remote state. This ordering is required: attaching the observer after `Store.initialize()` can miss the initial server hydration.

> [tab] React Component
>
> ```tsx theme={null}
> import { CKEditorCrdtEditor } from '@veltdev/ckeditor-crdt-react';
>
> function CollaborativeEditor() {
>   return (
>     <CKEditorCrdtEditor
>       editor={ClassicEditor}
>       editorId={DOCUMENT_ID}
>       initialContent="<p>Start writing here...</p>"
>       config={editorConfig}
>       cursorData={{ name: 'Ada', color: '#0f766e' }}
>       debounceMs={125}
>       onError={(error) => console.error('Collaboration error:', error)}
>     />
>   );
> }
> ```
>
> Standard CKEditor React props pass through except `data` and `onReady`. Use `onEditorReady` to receive the captured editor and `onCKEditorError` for CKEditor watchdog errors.

> [tab] React Hook
>
> Use `useCollaboration()` when you render CKEditor yourself:
>
> ```tsx theme={null}
> import { CKEditor } from '@ckeditor/ckeditor5-react';
> import { useCollaboration } from '@veltdev/ckeditor-crdt-react';
>
> function CollaborativeEditor() {
>   const {
>     editorRef,
>     manager,
>     primitives,
>     isLoading,
>     isSynced,
>     status,
>     error,
>   } = useCollaboration({
>     editorId: DOCUMENT_ID,
>     initialContent: '<p>Start writing here...</p>',
>     onError: (err) => console.error('Collaboration error:', err),
>   });
>
>   if (error) return <div>Error: {error.message}</div>;
>
>   return (
>     <div>
>       <div>Status: {isLoading ? 'loading' : status} | Synced: {isSynced ? 'Yes' : 'No'}</div>
>       <CKEditor
>         editor={ClassicEditor}
>         data=""
>         config={editorConfig}
>         onReady={(editor) => editorRef(editor)}
>         onAfterDestroy={() => editorRef(null)}
>       />
>     </div>
>   );
> }
> ```
>
> > [!WARNING]
> >
> > When using the hook, keep `data` empty and do not update it from React state. Forward `onAfterDestroy` to `editorRef(null)` so the manager releases a destroyed editor instance.

> [tab] Other Frameworks
>
> ```ts theme={null}
> import { createCollaboration } from '@veltdev/ckeditor-crdt';
>
> async function initializeCKEditor(client) {
>   const editor = await ClassicEditor.create(
>     document.querySelector('#editor')!,
>     editorConfig,
>   );
>
>   const manager = await createCollaboration({
>     editorId: DOCUMENT_ID,
>     veltClient: client,
>     editor,
>     initialContent: '<p>Start writing here...</p>',
>     debounceMs: 125,
>     cursorData: { name: 'Ada', color: '#0f766e' },
>     cursorsContainer: document.querySelector<HTMLElement>('#editor-surface'),
>     onError: (error) => console.error('Collaboration error:', error),
>   });
>
>   return { editor, manager };
> }
> ```

Set the Velt document context before rendering the editor. The React integration explicitly waits until Velt is initialized, a user is authenticated, the CKEditor instance is ready, and `enabled` is not `false`. Document context is a prerequisite, but the hook does not explicitly wait for it.

### Step 5: Status Monitoring (Optional)

The hook returns reactive `status`, `synced` / `isSynced`, `isLoading`, and `error` state. The component offers `onStatusChange` and `onManagerReady` callbacks.

> [tab] React / Next.js
>
> ```tsx theme={null}
> useEffect(() => {
>   if (!manager) return;
>
>   const offStatus = manager.onStatusChange(console.log);
>   const offSynced = manager.onSynced(console.log);
>
>   return () => {
>     offStatus();
>     offSynced();
>   };
> }, [manager]);
> ```

> [tab] Other Frameworks
>
> ```ts theme={null}
> const offStatus = manager.onStatusChange((status) => {
>   console.log('Status:', status);
> });
> const offSynced = manager.onSynced((synced) => {
>   console.log('Synced:', synced);
> });
>
> // Cleanup:
> offStatus();
> offSynced();
> ```

> [!NOTE]
>
> Do not disable local editing simply because the provider is connecting. Local HTML changes synchronize when the connection returns.

### Step 6: Version Management (Optional)

The hook keeps version metadata in React state and refreshes it after saves and restores:

> [tab] React / Next.js
>
> ```tsx theme={null}
> const {
>   versions,
>   saveVersion,
>   restoreVersion,
>   refreshVersions,
> } = useCollaboration({ editorId: DOCUMENT_ID });
>
> const versionId = await saveVersion('Approved copy');
> await refreshVersions();
> await restoreVersion(versionId);
> ```

> [tab] Other Frameworks
>
> ```ts theme={null}
> const versionId = await manager.saveVersion('Approved copy');
> let versions = await manager.getVersions();
>
> await manager.restoreVersion(versionId);
> versions = await manager.getVersions();
> ```

The methods are no-throw: when unavailable they resolve to `''`, `false`, or `[]`. A restore updates the shared HTML for every collaborator.

### Step 7: Force Reset Initial Content (Optional)

`initialContent` is normalized HTML and is applied once to a new empty shared document:

> [tab] React / Next.js
>
> ```tsx theme={null}
> <CKEditorCrdtEditor
>   editor={ClassicEditor}
>   editorId={DOCUMENT_ID}
>   config={editorConfig}
>   initialContent="<h2>Project brief</h2><p>Start collaborating...</p>"
> />
> ```
>
> For an explicit reset:
>
> ```tsx theme={null}
> <CKEditorCrdtEditor
>   editor={ClassicEditor}
>   editorId={DOCUMENT_ID}
>   config={editorConfig}
>   initialContent="<p>Fresh template</p>"
>   forceResetInitialContent
> />
> ```

> [tab] Other Frameworks
>
> Add the reset options to the manager creation call from Step 4:
>
> ```ts theme={null}
> const editor = await ClassicEditor.create(
>   document.querySelector('#editor')!,
>   editorConfig,
> );
>
> const manager = await createCollaboration({
>   editorId: DOCUMENT_ID,
>   veltClient: client,
>   editor,
>   initialContent: '<p>Fresh template</p>',
>   forceResetInitialContent: true,
> });
> ```

> [!WARNING]
>
> `forceResetInitialContent` replaces existing shared HTML. Do not enable it during normal initialization.

### Step 8: Configure Remote Cursors (Optional)

Remote carets and selections render in an overlay anchored to CKEditor's editable surface.

> [tab] React / Next.js
>
> ```tsx theme={null}
> <CKEditorCrdtEditor
>   editor={ClassicEditor}
>   editorId={DOCUMENT_ID}
>   config={editorConfig}
>   cursorData={{
>     name: user.name,
>     color: '#2563eb',
>     colorLight: '#2563eb33',
>   }}
> />
> ```
>
> Pass a custom `cursorsContainer`, or set it to `null` to disable overlay DOM while retaining awareness data.
>
> The hook also returns reactive `remoteCursors`:
>
> ```tsx theme={null}
> const { remoteCursors } = useCollaboration({
>   editorId: DOCUMENT_ID,
>   cursorData: { name: user.name, color: '#2563eb' },
> });
>
> return (
>   <ul>
>     {remoteCursors.map((cursor) => (
>       <li key={cursor.clientId} style={{ color: cursor.color }}>
>         {cursor.name}
>       </li>
>     ))}
>   </ul>
> );
> ```

> [tab] Other Frameworks
>
> Pass cursor identity and an optional overlay container when creating the manager:
>
> ```ts theme={null}
> const editor = await ClassicEditor.create(
>   document.querySelector('#editor')!,
>   editorConfig,
> );
>
> const manager = await createCollaboration({
>   editorId: DOCUMENT_ID,
>   veltClient: client,
>   editor,
>   cursorData: {
>     name: 'Ada Lovelace',
>     color: '#2563eb',
>     colorLight: '#2563eb33',
>   },
>   cursorsContainer: document.querySelector<HTMLElement>('#editor-surface'),
> });
>
> const offRemoteCursors = manager.onRemoteCursorsChange((cursors) => {
>   console.log('Remote cursors:', cursors);
> });
> ```
>
> Set `cursorsContainer: null` to keep awareness data without rendering the overlay. Call `offRemoteCursors()` during cleanup.

Optional host styling:

```css theme={null}
.velt-ckeditor-cursor-overlay {
  pointer-events: none;
}

.velt-ckeditor-remote-selection {
  border-radius: 2px;
  mix-blend-mode: multiply;
  pointer-events: none;
  position: absolute;
}

.velt-ckeditor-remote-cursor,
.velt-ckeditor-remote-cursor-caret {
  border-radius: 999px;
  min-height: 18px;
  pointer-events: none;
  position: absolute;
  width: 2px;
}

.velt-ckeditor-remote-cursor-label {
  background: white;
  border: 1px solid;
  border-left-width: 4px;
  border-radius: 6px;
  font-size: 12px;
  font-weight: 700;
  padding: 3px 8px;
  pointer-events: none;
  position: absolute;
  white-space: nowrap;
}

```

The built-in overlay creates `.velt-ckeditor-remote-cursor`, `.velt-ckeditor-remote-cursor-caret`, `.velt-ckeditor-remote-cursor-label`, and `.velt-ckeditor-remote-selection` elements. Cursor labels also carry the collaborator name in `data-velt-cursor-name` for selectors, tests, or custom styling.

### Step 9: CRDT Event Subscription (Optional)

> [tab] React / Next.js
>
> ```tsx theme={null}
> import { useEffect } from 'react';
> import { useVeltClient } from '@veltdev/react';
>
> const { client } = useVeltClient();
>
> useEffect(() => {
>   if (!client) return;
>
>   const subscription = client.getCrdtElement().on('updateData').subscribe((event) => {
>     if (event) console.log('CRDT update:', event);
>   });
>
>   return () => subscription?.unsubscribe?.();
> }, [client]);
> ```

> [tab] Other Frameworks
>
> ```ts theme={null}
> const crdtSubscription = client
>   .getCrdtElement()
>   .on('updateData')
>   .subscribe((event) => {
>     if (event) console.log('CRDT update:', event);
>   });
>
> // Cleanup:
> crdtSubscription.unsubscribe();
> ```

### Step 10: Custom Encryption (Optional)

> [tab] React / Next.js
>
> ```tsx theme={null}
> const encryptionProvider = {
>   encrypt: async ({ data }) => data.map((n) => n.toString()).join('__'),
>   decrypt: async ({ data }) => data.split('__').map((s) => parseInt(s, 10)),
> };
>
> <VeltProvider
>   apiKey="YOUR_API_KEY"
>   authProvider={authProvider}
>   encryptionProvider={encryptionProvider}
> >
>   ...
> </VeltProvider>
> ```

> [tab] Other Frameworks
>
> Set encryption before creating CKEditor collaboration:
>
> ```ts theme={null}
> client.setEncryptionProvider({
>   encrypt: async ({ data }) => data.map((n) => n.toString()).join('__'),
>   decrypt: async ({ data }) => data.split('__').map((s) => parseInt(s, 10)),
> });
> ```

See also: [setEncryptionProvider()](https://velt.dev/docs/api-reference/sdk/api/api-methods.md#setencryptionprovider-encryptionprovider)
· [VeltEncryptionProvider](https://velt.dev/docs/api-reference/sdk/models/data-models.md#veltencryptionprovider)
· [EncryptConfig](https://velt.dev/docs/api-reference/sdk/models/data-models.md#encryptconfig)
· [DecryptConfig](https://velt.dev/docs/api-reference/sdk/models/data-models.md#decryptconfig)

### Step 11: Error Handling (Optional)

Use `onError` for collaboration errors and `onCKEditorError` for official CKEditor React watchdog errors:

> [tab] React / Next.js
>
> ```tsx theme={null}
> <CKEditorCrdtEditor
>   editor={ClassicEditor}
>   editorId={DOCUMENT_ID}
>   config={editorConfig}
>   onError={(error) => console.error('Collaboration:', error)}
>   onCKEditorError={(error, details) => console.error('CKEditor:', error, details)}
> />
> ```

> [tab] Other Frameworks
>
> The direct manager reports collaboration errors through `onError`. Handle CKEditor creation errors separately:
>
> ```ts theme={null}
> try {
>   const editor = await ClassicEditor.create(
>     document.querySelector('#editor')!,
>     editorConfig,
>   );
>
>   const manager = await createCollaboration({
>     editorId: DOCUMENT_ID,
>     veltClient: client,
>     editor,
>     onError: (error) => {
>       console.error('CKEditor collaboration error:', error);
>     },
>   });
> } catch (error) {
>   console.error('CKEditor creation failed:', error);
> }
> ```

### Step 12: Access Yjs Internals (Advanced)

The hook's `primitives` object groups lower-level resources:

> [tab] React / Next.js
>
> ```tsx theme={null}
> const { primitives } = useCollaboration({ editorId: DOCUMENT_ID });
>
> const editor = primitives?.editor;
> const doc = primitives?.doc;
> const xmlFragment = primitives?.xmlFragment;
> const provider = primitives?.provider;
> const providerAdapter = primitives?.providerAdapter;
> const awareness = primitives?.awareness;
> const undoManager = primitives?.undoManager;
> const store = primitives?.store;
> ```
>
> Read or replace normalized shared HTML with the manager:
>
> ```tsx theme={null}
> const html = manager.getHtml();
> manager.setHtml('<p>Replacement content</p>');
> ```

> [tab] Other Frameworks
>
> ```ts theme={null}
> const doc = manager.getDoc();
> const xmlFragment = manager.getXmlFragment();
> const provider = manager.getProvider();
> const providerAdapter = manager.getProviderAdapter();
> const awareness = manager.getAwareness();
> const undoManager = manager.getUndoManager();
> const store = manager.getStore();
>
> const html = manager.getHtml();
> manager.setHtml('<p>Replacement content</p>');
>
> // Collaboration-aware application controls:
> undoManager?.undo();
> undoManager?.redo();
> ```

The CKEditor source build includes its `Undo` plugin for the native toolbar. For custom collaboration-aware controls, use the manager's Yjs `UndoManager`; do not trigger both history paths from the same action.

### Step 13: Enable, Disable, and Cleanup

> [tab] React / Next.js
>
> ```tsx theme={null}
> const collaboration = useCollaboration({
>   editorId: DOCUMENT_ID,
>   enabled: Boolean(user && documentReady),
> });
> ```
>
> Changing `enabled` to `false` destroys the current manager; changing it back reinitializes collaboration. The component and hook also clean up automatically on unmount or dependency changes, and the hook exposes `destroy()` for early teardown.
>
> If you render CKEditor yourself, the editor remains application-owned. Forward `onAfterDestroy` to `editorRef(null)` and destroy CKEditor according to its normal lifecycle.

> [tab] Other Frameworks
>
> Keep every disposer. Destroy the manager before destroying the application-owned CKEditor instance:
>
> ```ts theme={null}
> crdtSubscription.unsubscribe();
> offRemoteCursors();
> offStatus();
> offSynced();
> initSubscription.unsubscribe();
>
> manager.destroy();
> await editor.destroy();
> ```

## Notes

* **Uncontrolled content**: The CRDT manager owns CKEditor data; do not synchronize it through React state.
* **CKEditor configuration**: Include every plugin used by the toolbar and provide the correct CKEditor license key.
* **Unique editorId**: Collaborators must share the same Velt document context and editor ID.
* **HTML data model**: The adapter stores normalized HTML in a Yjs XML fragment and preserves local selection while applying remote content.
* **Initial remote hydration**: The manager registers its Yjs fragment observer before `Store.initialize()`. Preserve that ordering in custom integrations so server state loaded during initialization is not missed.
* **Remote cursors**: The adapter renders an overlay and also exposes normalized cursor state for custom UI.
* **StrictMode-safe**: The hook destroys stale managers during React remounts and dependency changes.
* **React readiness**: The hook waits for Velt initialization, an authenticated user, the CKEditor instance, and `enabled`. It does not explicitly wait for document context, so set the document first.
* **Editor-first direct setup**: Other frameworks create CKEditor before creating the collaboration manager.
* **Other Frameworks lifecycle**: Unsubscribe callbacks, destroy the manager, and then destroy the application-owned CKEditor instance.
* **One dependency instance**: Deduplicate `ckeditor5`, `yjs`, `y-protocols`, and `lib0` in linked or monorepo builds.
* **Initial content**: Seed HTML is applied once unless an explicit reset is requested.
* **Production authentication**: Use Velt's recommended server-backed authentication flow.

## Testing and Debugging

**To test collaboration:**

1. Log in as two unique users in separate browser profiles.
2. Open the same Velt document and CKEditor `editorId`.
3. Edit text, headings, marks, and lists in both windows.
4. Verify remote selections, version restore, reconnection, and persistence after reload.

**Common issues:**

* Toolbar item unavailable: Add its CKEditor plugin to `config.plugins`.
* License error: Provide `GPL` only for a GPL-compatible use, otherwise use your commercial license key.
* Content does not sync: Remove controlled `data` updates and confirm both clients use the same document and `editorId`.
* Manager never appears: Confirm the document context was set first, Velt is initialized, a user is identified, and CKEditor's `onReady` fired. The document is a prerequisite, not an explicit hook wait condition.
* Cursors not visible: Ensure `cursorsContainer` is not `null` and the editable element is mounted.
* Stale manager after watchdog restart: Forward `onAfterDestroy` when using the hook directly.

> [!TIP]
>
> Use the [VeltCrdtStoreMap debugging interface](https://velt.dev/docs/realtime-collaboration/crdt/setup/core.md#veltcrdtstoremap) to inspect the shared XML and provider state.

## Complete Example

> [tab] React / Next.js
>
> ```tsx Complete App.tsx expandable lines theme={null}
> import { useEffect, useState } from 'react';
> import { CKEditorCrdtEditor } from '@veltdev/ckeditor-crdt-react';
> import type { CollaborationManager, SyncStatus } from '@veltdev/ckeditor-crdt-react';
> import {
>   VeltProvider,
>   useCurrentUser,
>   useSetDocuments,
> } from '@veltdev/react';
> import {
>   ClassicEditor,
>   Essentials,
>   Paragraph,
>   Bold,
>   Italic,
>   Underline,
>   Heading,
>   List,
>   Highlight,
>   FontColor,
>   FontBackgroundColor,
>   Undo,
>   GeneralHtmlSupport,
> } from 'ckeditor5';
> import 'ckeditor5/ckeditor5.css';
>
> const DOCUMENT_ID = 'ckeditor-crdt-demo-doc-1';
>
> const editorConfig = {
>   licenseKey: 'GPL',
>   plugins: [
>     Essentials,
>     Paragraph,
>     Bold,
>     Italic,
>     Underline,
>     Heading,
>     List,
>     Highlight,
>     FontColor,
>     FontBackgroundColor,
>     Undo,
>     GeneralHtmlSupport,
>   ],
>   toolbar: [
>     'undo', 'redo', '|',
>     'heading', '|',
>     'bold', 'italic', 'underline', '|',
>     'fontColor', 'fontBackgroundColor', 'highlight', '|',
>     'bulletedList', 'numberedList',
>   ],
> };
>
> const USER = {
>   userId: 'ada',
>   name: 'Ada Lovelace',
>   email: 'ada@example.com',
>   organizationId: 'my-org-id',
>   color: '#2563eb',
> };
>
> const authProvider = {
>   user: USER,
>   retryConfig: { retryCount: 3, retryDelay: 1000 },
>   generateToken: async () => {
>     const response = await fetch('/api/velt/token', {
>       method: 'POST',
>       headers: { 'Content-Type': 'application/json' },
>       body: JSON.stringify(USER),
>     });
>     const { token } = await response.json();
>     return token;
>   },
> };
>
> function CollaborativeEditor() {
>   const user = useCurrentUser();
>   const [status, setStatus] = useState<SyncStatus>('connecting');
>   const [manager, setManager] = useState<CollaborationManager | null>(null);
>   const [versionName, setVersionName] = useState('');
>   const { setDocuments } = useSetDocuments();
>   const [documentReady, setDocumentReady] = useState(false);
>
>   useEffect(() => {
>     if (!user) {
>       setDocumentReady(false);
>       return;
>     }
>     setDocuments([{
>       id: DOCUMENT_ID,
>       metadata: { documentName: 'CKEditor CRDT Demo' },
>     }]);
>     setDocumentReady(true);
>   }, [user, setDocuments]);
>
>   if (!user || !documentReady) return <p>Preparing collaboration...</p>;
>
>   const saveVersion = async () => {
>     if (!manager || !versionName.trim()) return;
>     await manager.saveVersion(versionName.trim());
>     setVersionName('');
>   };
>
>   return (
>     <main>
>       <p>Status: {status} | Synced: {manager?.synced ? 'Yes' : 'No'}</p>
>       <input
>         value={versionName}
>         onChange={(event) => setVersionName(event.target.value)}
>         placeholder="Version name"
>       />
>       <button onClick={saveVersion}>Save version</button>
>       <CKEditorCrdtEditor
>         editor={ClassicEditor}
>         editorId={DOCUMENT_ID}
>         config={editorConfig}
>         initialContent="<h2>Shared document</h2><p>Start collaborating...</p>"
>         cursorData={{ name: user.name, color: '#2563eb' }}
>         onStatusChange={setStatus}
>         onManagerReady={setManager}
>         onError={console.error}
>       />
>     </main>
>   );
> }
>
> export default function App() {
>   return (
>     <VeltProvider
>       apiKey="YOUR_VELT_API_KEY"
>       authProvider={authProvider}
>     >
>       <CollaborativeEditor />
>     </VeltProvider>
>   );
> }
> ```

> [tab] Other Frameworks
>
> Create the CKEditor instance first. Only after it resolves should you pass that exact instance to the framework-agnostic collaboration manager.
>
> ```ts Complete app.ts expandable lines theme={null}
> import { initVelt } from '@veltdev/client';
> import {
>   createCollaboration,
>   type CollaborationManager,
> } from '@veltdev/ckeditor-crdt';
> import {
>   ClassicEditor,
>   Essentials,
>   Paragraph,
>   Bold,
>   Italic,
>   Underline,
>   Heading,
>   List,
>   Highlight,
>   FontColor,
>   FontBackgroundColor,
>   Undo,
>   GeneralHtmlSupport,
> } from 'ckeditor5';
> import 'ckeditor5/ckeditor5.css';
>
> const DOCUMENT_ID = 'ckeditor-crdt-demo-doc-1';
>
> const editorConfig = {
>   licenseKey: 'GPL',
>   plugins: [
>     Essentials,
>     Paragraph,
>     Bold,
>     Italic,
>     Underline,
>     Heading,
>     List,
>     Highlight,
>     FontColor,
>     FontBackgroundColor,
>     Undo,
>     GeneralHtmlSupport,
>   ],
>   toolbar: [
>     'undo', 'redo', '|',
>     'heading', '|',
>     'bold', 'italic', 'underline', '|',
>     'fontColor', 'fontBackgroundColor', 'highlight', '|',
>     'bulletedList', 'numberedList',
>   ],
> };
>
> let resources: {
>   editor: Awaited<ReturnType<typeof ClassicEditor.create>>;
>   manager: CollaborationManager;
>   offStatus: () => void;
>   offSynced: () => void;
>   offRemoteCursors: () => void;
>   crdtSubscription: { unsubscribe: () => void };
> } | null = null;
>
> async function initializeCKEditor(client) {
>   const editor = await ClassicEditor.create(
>     document.querySelector('#editor')!,
>     editorConfig,
>   );
>
>   const manager = await createCollaboration({
>     editorId: DOCUMENT_ID,
>     veltClient: client,
>     editor,
>     initialContent: '<h2>Shared document</h2><p>Start collaborating...</p>',
>     debounceMs: 125,
>     cursorData: {
>       name: 'Ada Lovelace',
>       color: '#2563eb',
>       colorLight: '#2563eb33',
>     },
>     cursorsContainer: document.querySelector<HTMLElement>('#editor-surface'),
>     onError: (error) => console.error('Collaboration error:', error),
>   });
>
>   if (!manager.getXmlFragment() || !manager.getEditor()) {
>     manager.destroy();
>     await editor.destroy();
>     throw new Error('CKEditor collaboration did not initialize');
>   }
>
>   const offStatus = manager.onStatusChange((status) => {
>     document.getElementById('status')!.textContent = status;
>   });
>   const offSynced = manager.onSynced((synced) => {
>     document.getElementById('synced')!.textContent = synced ? 'Yes' : 'No';
>   });
>   const offRemoteCursors = manager.onRemoteCursorsChange((cursors) => {
>     console.log('Remote cursors:', cursors);
>   });
>   const crdtSubscription = client
>     .getCrdtElement()
>     .on('updateData')
>     .subscribe((event) => {
>       if (event) console.log('CRDT update:', event);
>     });
>
>   document.getElementById('undo')?.addEventListener('click', () => {
>     manager.getUndoManager()?.undo();
>   });
>   document.getElementById('redo')?.addEventListener('click', () => {
>     manager.getUndoManager()?.redo();
>   });
>   document.getElementById('save-version')?.addEventListener('click', async () => {
>     const versionId = await manager.saveVersion('Approved copy');
>     console.log('Saved version:', versionId, await manager.getVersions());
>   });
>
>   resources = {
>     editor,
>     manager,
>     offStatus,
>     offSynced,
>     offRemoteCursors,
>     crdtSubscription,
>   };
> }
>
> const client = await initVelt('YOUR_VELT_API_KEY');
>
> await client.identify({
>   userId: 'ada',
>   name: 'Ada Lovelace',
>   email: 'ada@example.com',
>   organizationId: 'my-org-id',
> });
>
> await client.setDocument(DOCUMENT_ID, {
>   documentName: 'CKEditor CRDT Demo',
> });
>
> let initializationStarted = false;
> const initSubscription = client.getVeltInitState().subscribe((isReady) => {
>   if (!isReady || initializationStarted) return;
>   initializationStarted = true;
>   void initializeCKEditor(client);
> });
>
> window.addEventListener('beforeunload', () => {
>   resources?.crdtSubscription.unsubscribe();
>   resources?.offRemoteCursors();
>   resources?.offStatus();
>   resources?.offSynced();
>   initSubscription.unsubscribe();
>
>   resources?.manager.destroy();
>   void resources?.editor.destroy();
> });
> ```

## How It Works

1. React applications initialize Velt through an authenticated `VeltProvider`; other frameworks use `initVelt()`. Both authenticate before setting the shared document.
2. The React hook explicitly waits for Velt initialization, an authenticated user, the CKEditor instance, and `enabled`. The document remains a prerequisite but is not an explicit hook wait condition.
3. Direct integrations create CKEditor first and pass the resolved editor instance to `createCollaboration()`.
4. The manager opens a Velt CRDT store keyed by `editorId`, registers the fragment observer, and only then initializes the Store so initial remote HTML is observed.
5. CKEditor model changes are debounced into shared transactions; remote changes update CKEditor while preserving the local selection when possible.
6. Awareness carries user and selection metadata for remote cursor overlays, and Velt persists CRDT updates and named versions.

## APIs

### React: CKEditorCrdtEditor

| Prop | Type | Required | Description |
| - | - | - | - |
| `editor` | `unknown` | Yes | CKEditor constructor, usually `ClassicEditor`. |
| `editorId` | `string` | No\* | Unique key for the shared editor. |
| `documentId` | `string` | No\* | Used as `editorId` when omitted. |
| `config` | `Record<string, unknown>` | No | CKEditor plugins, toolbar, and license configuration. |
| `veltClient` | `Velt` | No | Overrides the context client. |
| `initialContent` | `string` | No | Initial HTML for a new document. |
| `forceResetInitialContent` | `boolean` | No | Replaces shared HTML during initialization. |
| `cursorData` | `CKEditorCursorData` | No | Local cursor identity and colors. |
| `cursorsContainer` | `HTMLElement \| null` | No | Overlay container; `null` disables overlay DOM. |
| `debounceMs` | `number` | No | Editor-to-CRDT write debounce. |
| `onError` | `(error: Error) => void` | No | Collaboration errors. |
| `onCKEditorError` | `function` | No | CKEditor React watchdog errors. |
| `onStatusChange` | `(status: SyncStatus) => void` | No | Status callback. |
| `onManagerReady` / `onEditorReady` | `function` | No | Receives manager or editor. |

\*Provide either `editorId` or `documentId`. Other CKEditor React props pass through except CRDT-owned `data` and `onReady`.

### React: useCollaboration()

| Return | Type | Description |
| - | - | - |
| `editorRef` | `(editor \| null) => void` | Captures or releases CKEditor. |
| `manager` | `CollaborationManager \| null` | Base manager. |
| `primitives` | `CKEditorCollaborationPrimitives \| null` | CKEditor, Yjs, and Velt resources. |
| `isLoading` | `boolean` | Initialization state. |
| `synced` / `isSynced` | `boolean` | Initial backend sync state. |
| `status` | `SyncStatus` | Connection status. |
| `error` | `Error \| null` | Latest error. |
| `versions` | `Version[]` | Saved versions. |
| `remoteCursors` | `CKEditorRemoteCursor[]` | Reactive remote cursor metadata. |
| `saveVersion(name)` | `Promise<string>` | Saves a version. |
| `restoreVersion(id)` | `Promise<boolean>` | Restores a version. |
| `refreshVersions()` | `Promise<Version[]>` | Reloads versions. |
| `destroy()` | `void` | Destroys collaboration early. |

### Other Frameworks: createCollaboration()

```ts theme={null}
import { createCollaboration } from '@veltdev/ckeditor-crdt';

const editor = await ClassicEditor.create(
  document.querySelector('#editor')!,
  editorConfig,
);

const manager = await createCollaboration({
  editorId: DOCUMENT_ID,
  veltClient: client,
  editor,
  initialContent: '<p>Start writing here...</p>',
  debounceMs: 125,
  cursorData: { name: 'Ada', color: '#0f766e' },
  cursorsContainer: document.querySelector<HTMLElement>('#editor-surface'),
  onError: console.error,
});
```

| Config | Type | Required | Description |
| - | - | - | - |
| `editorId` | `string` | Yes | Stable key for the shared CKEditor document. |
| `veltClient` | `Velt` | Yes | Initialized, authenticated Velt client with a current document. |
| `editor` | `CKEditorEditorLike` | Yes | CKEditor instance created before the manager. |
| `initialContent` | `string` | No | Normalized HTML seed for a new, empty shared document. |
| `forceResetInitialContent` | `boolean` | No | Explicitly replaces existing shared HTML. |
| `cursorData` | `CKEditorCursorData` | No | Awareness identity and cursor colors. |
| `cursorsContainer` | `HTMLElement \| null` | No | Cursor overlay host; `null` disables overlay DOM. |
| `debounceMs` | `number` | No | Editor-to-CRDT write debounce. |
| `onError` | `(error: Error) => void` | No | Receives non-fatal initialization and runtime errors. |

### CollaborationManager Methods

| Method or property | Description |
| - | - |
| `initialize()` / `destroy()` | Starts or destroys collaboration. |
| `initialized`, `synced`, `status` | Current manager state. |
| `onStatusChange(callback)` / `onSynced(callback)` | Status subscriptions. |
| `getHtml()` / `setHtml(html)` | Reads or replaces normalized HTML. |
| `getRemoteCursors()` | Reads remote cursor metadata. |
| `onRemoteCursorsChange(callback)` | Subscribes to cursor metadata. |
| `getDoc()` / `getXmlFragment()` | Returns shared Yjs data. |
| `getProvider()` / `getProviderAdapter()` | Returns provider objects. |
| `getAwareness()` / `getUndoManager()` | Returns awareness and undo manager. |
| `getStore()` / `getEditor()` | Returns Velt store and CKEditor instance. |
| `saveVersion(name)` / `getVersions()` | Saves or lists versions. |
| `restoreVersion(id)` / `setStateFromVersion(version)` | Restores shared state. |

### Exported Types and Helpers

* `CollaborationConfig`, `UseCollaborationConfig` / `UseCKEditorCrdtConfig`
* `UseCollaborationReturn` / `CKEditorCrdtHookResult`
* `CKEditorCrdtEditorProps`, `CKEditorCollaborationPrimitives`, `CKEditorEditorLike`
* `CKEditorCursorData`, `CKEditorCursorRect`, `CKEditorCursorSelection`, `CKEditorRemoteCursor`
* `SyncStatus`, `Unsubscribe`, `Version`
* `normalizeHtml()`, `readHtmlFromXmlFragment()`, `writeHtmlToXmlFragment()`
* `hasSharedHtml()`, `applyInitialContent()`


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.