@veltdev/lexical-crdt-react and @veltdev/lexical-crdt libraries enable real-time collaborative editing on Lexical Editors. The collaboration editing engine is built on top of Yjs, the official @lexical/yjs binding, and 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
- Optional: TypeScript for type safety
Setup
Step 1: Install Dependencies
Install the required packages:- 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: Initialize Collaborative Editor
- Set the Lexical composer’s
editorStatetonullbecause collaboration owns the document state. - Add the collaboration hook or plugin inside
<LexicalComposer>so it can read the editor fromLexicalComposerContext. - Pass an
editorIdto uniquely identify each editor instance you have in your app.
- React Component
- React Hook
- Other Frameworks
Use
LexicalCollaborationPlugin when you only need lifecycle wiring and do not need to render reactive collaboration state:Step 4: Use an Explicit Editor Instance in React (Optional)
If you create and hold aLexicalEditor instance outside LexicalComposer, use useCollaboration() and pass the instance explicitly:
Step 5: Status Monitoring (Optional)
- React / Next.js
- Other Frameworks
Both hooks return reactive For event-style subscriptions, use the manager:
status, isSynced, and error state:Step 6: Version Management (Optional)
Save and restore named snapshots through the manager. Restored versions are applied to every connected user in real time.Version object, use the lower-level state application API instead of restoreVersion():
Step 7: Force Reset Initial Content (Optional)
By default,initialContent is applied exactly once when the shared document is brand new. Use a stringified serialized Lexical editor state. React wrappers may also accept the wrapper’s documented plain-text convenience form.
- React / Next.js
- Other Frameworks
Step 8: Style Collaboration Cursors
Remote carets, labels, and text selections render through Lexical’s collaboration cursor layer. Pass user identity throughcursorData, then style the Lexical collaboration theme classes:
- React / Next.js
- Other Frameworks
Step 9: CRDT Event Subscription (Optional)
Listen for real-time CRDT data events usinggetCrdtElement().on() from the Velt client.
- React / Next.js
- Other Frameworks
Step 10: Custom Encryption (Optional)
Encrypt CRDT data before it is stored in Velt by registering a custom encryption provider. For CRDT methods, input data is of typeUint8Array | number[].
- React / Next.js
- Other Frameworks
Step 11: Error Handling (Optional)
Pass anonError callback to handle initialization or runtime errors. The reactive error state is also available for rendering.
- React / Next.js
- Other Frameworks
Step 12: Access Yjs Internals (Advanced)
The hooks return lower-level Lexical, Yjs, and Velt handles throughprimitives:
- React / Next.js
- Other Frameworks
@lexical/yjs manually alongside it.
Step 13: Cleanup
- React / Next.js
- Other Frameworks
The hooks destroy the manager automatically when the component unmounts or when the
editorId, editor, or Velt client changes. Pass enabled: false to tear down collaboration early while keeping the component mounted.Notes
- Unique editorId: Use a unique
editorIdper editor instance. - Composer state: Set
editorState: nullbecause collaboration owns the shared Lexical state. - Composer hook: Use
useLexicalComposerCollaboration()inside<LexicalComposer>. - Explicit editor: Use
useCollaboration()only when you already hold aLexicalEditorinstance. - Plugin form: Use
LexicalCollaborationPluginwhen you do not need reactive status, error, or manager values. - Cursor identity: Pass
cursorDataso remote carets and selections can display user information. - Lifecycle: The hooks wait for the Lexical root element, Velt initialization, and an authenticated user before creating the manager.
- Other Frameworks lifecycle: Create and attach Lexical first, then create one manager; destroy the manager before discarding the editor.
- Collaborative history: Do not register Lexical’s normal history plugin. Use the binding’s Yjs-backed undo manager from
manager.getUndoManager(). - Debounced writes: The Store default is
debounceMs: 0, so backend writes are immediate when the option is omitted. Lexical’s separate snapshot-flush scheduler falls back to 250 ms after applying seeded or prefetched snapshot state. - Dependency deduplication: Keep one copy of
lexical, each@lexical/*package, andyjsin the application bundle. - Production authentication: Do not expose privileged server tokens in browser code; use Velt’s recommended server-backed authentication flow.
Testing and Debugging
To test collaboration:- Log in as two unique users in two separate browser profiles and open the same Velt document and Lexical
editorIdin both. - Edit text, formatting, headings, or lists in one window and verify the changes and remote cursor appear in the other.
- Make concurrent edits and verify they merge at the character level.
- Reload both windows and verify the shared document persists.
- Editor not loading: Confirm the Velt client is initialized, the user is authenticated, and the collaboration bridge renders inside
<LexicalComposer>. - Initial content conflicts: Set the composer’s
editorStatetonull. - Cursors not appearing: Pass
cursorDataand include the Lexical collaboration theme classes in your editor theme and CSS. - Changes not syncing: Confirm both users have the same Velt document context and
editorId. - Manual reconnect: Use
manager.getProvider()?.connect()to reconnect ormanager.getProvider()?.disconnect()to test an offline transition.
Complete Example
- React / Next.js
- Other Frameworks
A complete collaborative Lexical editor with user login, status display, cursor identity, and version management.App.tsxEditor.tsx
Complete App.tsx
Complete Editor.tsx
How It Works
- Velt initialization and authentication happen through
VeltProviderin React orinitVelt()andidentify()in other frameworks. Both paths establish the same stable document context. - React wrappers obtain the editor from
LexicalComposerContextor accept an explicit editor. Other frameworks create the Lexical editor, register its nodes, and attach its root before creating collaboration. - The collaboration manager creates a Velt CRDT store backed by a Yjs document and a shared
Y.XmlText. - The official
@lexical/yjsbinding connects Lexical updates to the sharedY.XmlText, so text, formatting, headings, and concurrent edits merge at the character level. - Yjs awareness drives the manager-owned remote cursor overlay, while the binding’s
Y.UndoManagerprovides collaboration-aware undo and redo. - Version management saves the complete Yjs state as a named snapshot that can be restored for every connected user.
- React cleanup is automatic when the wrapper unmounts or collaboration is disabled. Other frameworks unsubscribe callbacks and destroy the manager before discarding Lexical.
APIs
React: useLexicalComposerCollaboration()
The primary hook for applications using<LexicalComposer>. It reads the editor from context, waits for the root element, and manages the collaboration lifecycle.
- Signature:
useLexicalComposerCollaboration(config: UseLexicalComposerCollaborationConfig): UseCollaborationReturn - Alias:
useLexicalCollaboration - Params:
editorId: Unique identifier for the collaborative editor.veltClient: Optional explicit Velt client. Falls back toVeltProvidercontext.enabled: Delay initialization or destroy active collaboration. Default:true.initialContent: Serialized Lexical editor state or plain text for a brand-new document.forceResetInitialContent: Replace existing shared content during initialization. Default:false.cursorData: Cursor name, color, and custom awareness data.debounceMs: Throttle interval in milliseconds for backend writes. Store default:0ms (immediate). The internal snapshot-flush scheduler separately falls back to 250 ms when this option is omitted.onError: Callback for non-fatal collaboration errors.
- Returns:
primitives: Lexical, Yjs, and Velt integration handles.nullwhile loading.isLoading:trueuntil the manager is initialized.isSynced:trueafter initial backend sync completes.status:'connecting','connected', or'disconnected'.error: Most recent error, ornull.manager: UnderlyingCollaborationManager, ornullbefore initialization.
React: useCollaboration()
Explicit-editor form of the React hook. It has the same lifecycle and return values, but requires aneditor: LexicalEditor config value.
React: LexicalCollaborationPlugin
Null-rendering component form ofuseLexicalComposerCollaboration(). Use it when you only need collaboration lifecycle wiring.
Other Frameworks: createCollaboration()
The framework-agnosticcreateCollaboration() factory from @veltdev/lexical-crdt creates and initializes the manager for an existing LexicalEditor.
- Signature:
createCollaboration(config: CollaborationConfig): Promise<CollaborationManager> - Required config:
editorId,editor, and an initialized, authenticatedveltClient. - Optional config:
initialContent,cursorData,debounceMs(Store default: 0 ms),forceResetInitialContent, andonError. The internal snapshot-flush scheduler separately falls back to 250 ms whendebounceMsis omitted. - Cleanup: Call
manager.destroy()before disposing the editor.
LexicalCollaborationPrimitives
LexicalProviderAdapter
LexicalProviderAdapter is exported from @veltdev/lexical-crdt. It adapts Velt’s SyncProvider events to the provider interface expected by @lexical/yjs; the collaboration manager creates it automatically, so most applications should access the existing adapter through manager.getLexicalProvider() instead of constructing another one.
Version
CollaborationManager Methods
manager.onStatusChange()
Subscribe to connection status changes.- Signature:
manager.onStatusChange(callback: (status: SyncStatus) => void): Unsubscribe
manager.onSynced()
Subscribe to initial sync state changes.- Signature:
manager.onSynced(callback: (synced: boolean) => void): Unsubscribe
manager.initialized
Whether initialization has completed.- Returns:
boolean
manager.synced
Whether initial backend sync has completed.- Returns:
boolean
manager.status
Current connection status.- Returns:
SyncStatus('connecting' | 'connected' | 'disconnected')
manager.saveVersion()
Save a named snapshot and return its version ID.- Signature:
manager.saveVersion(name: string): Promise<string>
manager.getVersions()
List all saved versions.- Returns:
Promise<Version[]>
manager.restoreVersion()
Restore a saved version for all connected users.- Signature:
manager.restoreVersion(versionId: string): Promise<boolean>
manager.setStateFromVersion()
Apply aVersion object’s state to the shared document.
- Signature:
manager.setStateFromVersion(version: Version): Promise<void>
manager.getEditor()
Get the bound Lexical editor.- Returns:
LexicalEditor | null
manager.getBinding()
Get the@lexical/yjs binding.
- Returns:
Binding | null
manager.getLexicalProvider()
Get the provider adapter used by the Lexical binding.- Returns:
Provider | null
manager.getDoc()
Get the underlying Yjs document.- Returns:
Y.Doc | null
manager.getXmlText()
Get the sharedY.XmlText bound to Lexical content.
- Returns:
Y.XmlText | null
manager.getXmlFragment()
Alias ofgetXmlText() provided for parity with other CRDT integrations.
- Returns:
Y.XmlText | null
manager.getProvider()
Get the Velt sync provider.- Returns:
SyncProvider | null
manager.getAwareness()
Get the Yjs awareness instance.- Returns:
Awareness | null
manager.getUndoManager()
Get the collaborative Yjs undo manager.- Returns:
Y.UndoManager | null
manager.getStore()
Get the underlying Velt CRDT store.- Returns:
Store<string> | null
manager.destroy()
Destroy the binding, provider adapter, awareness state, listeners, and store. The React hooks call this automatically.- Returns:
void
Custom Encryption
Encrypt CRDT data before it is stored in Velt by registering a custom encryption provider.Limitations
- Content written through the CRDT REST API is not currently materialized into the Lexical editor. Browser-to-REST reads work, but REST-to-browser content does not.
- Online and offline concurrent editor changes merge at the character level through the official
@lexical/yjsbinding.

