@veltdev/ckeditor-crdt-react and @veltdev/ckeditor-crdt libraries enable real-time collaborative editing in CKEditor 5. The collaboration engine is built on Yjs and the Velt SDK.
Prerequisites
- Node.js (v14 or higher)
- A Velt account with an API key (sign up)
- 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
- React / Next.js
- Other Frameworks
Step 2: Setup Velt
Initialize Velt, authenticate a user, and set a stable document context before collaboration starts. Follow the Velt Setup Docs for the production token endpoint used byauthProvider.
- React / Next.js
- Other Frameworks
Step 3: Configure CKEditor
Import the editor constructor, the features your build needs, and CKEditor styles:GPL when required by your distribution.
CKEditor serializes highlights as <mark> elements. This markup persists through realtime sync, force reset, and version restore:
Step 4: Initialize Collaborative Editor
- Pass an
editorIdto uniquely identify the shared editor. - Do not pass CKEditor React’s
dataoronReadyto the drop-in component; the CRDT wrapper owns content and editor capture. - Use
initialContentas HTML seed content for a new shared document.
Store.initialize() can miss the initial server hydration.
- React Component
- React Hook
- Other Frameworks
data and onReady. Use onEditorReady to receive the captured editor and onCKEditorError for CKEditor watchdog errors.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 reactivestatus, synced / isSynced, isLoading, and error state. The component offers onStatusChange and onManagerReady callbacks.
- React / Next.js
- Other Frameworks
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:- React / Next.js
- Other Frameworks
'', 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:
- React / Next.js
- Other Frameworks
Step 8: Configure Remote Cursors (Optional)
Remote carets and selections render in an overlay anchored to CKEditor’s editable surface.- React / Next.js
- Other Frameworks
cursorsContainer, or set it to null to disable overlay DOM while retaining awareness data.The hook also returns reactive remoteCursors:.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)
- React / Next.js
- Other Frameworks
Step 10: Custom Encryption (Optional)
- React / Next.js
- Other Frameworks
Step 11: Error Handling (Optional)
UseonError for collaboration errors and onCKEditorError for official CKEditor React watchdog errors:
- React / Next.js
- Other Frameworks
Step 12: Access Yjs Internals (Advanced)
The hook’sprimitives object groups lower-level resources:
- React / Next.js
- Other Frameworks
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
- React / Next.js
- Other Frameworks
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.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, andlib0in 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:- Log in as two unique users in separate browser profiles.
- Open the same Velt document and CKEditor
editorId. - Edit text, headings, marks, and lists in both windows.
- Verify remote selections, version restore, reconnection, and persistence after reload.
- Toolbar item unavailable: Add its CKEditor plugin to
config.plugins. - License error: Provide
GPLonly for a GPL-compatible use, otherwise use your commercial license key. - Content does not sync: Remove controlled
dataupdates and confirm both clients use the same document andeditorId. - Manager never appears: Confirm the document context was set first, Velt is initialized, a user is identified, and CKEditor’s
onReadyfired. The document is a prerequisite, not an explicit hook wait condition. - Cursors not visible: Ensure
cursorsContaineris notnulland the editable element is mounted. - Stale manager after watchdog restart: Forward
onAfterDestroywhen using the hook directly.
Complete Example
- React / Next.js
- Other Frameworks
Complete App.tsx
How It Works
- React applications initialize Velt through an authenticated
VeltProvider; other frameworks useinitVelt(). Both authenticate before setting the shared document. - 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. - Direct integrations create CKEditor first and pass the resolved editor instance to
createCollaboration(). - 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. - CKEditor model changes are debounced into shared transactions; remote changes update CKEditor while preserving the local selection when possible.
- Awareness carries user and selection metadata for remote cursor overlays, and Velt persists CRDT updates and named versions.
APIs
React: CKEditorCrdtEditor
*Provide either
editorId or documentId. Other CKEditor React props pass through except CRDT-owned data and onReady.
React: useCollaboration()
Other Frameworks: createCollaboration()
CollaborationManager Methods
Exported Types and Helpers
CollaborationConfig,UseCollaborationConfig/UseCKEditorCrdtConfigUseCollaborationReturn/CKEditorCrdtHookResultCKEditorCrdtEditorProps,CKEditorCollaborationPrimitives,CKEditorEditorLikeCKEditorCursorData,CKEditorCursorRect,CKEditorCursorSelection,CKEditorRemoteCursorSyncStatus,Unsubscribe,VersionnormalizeHtml(),readHtmlFromXmlFragment(),writeHtmlToXmlFragment()hasSharedHtml(),applyInitialContent()

