Overview
The @lexical/yjs package enables real-time collaborative editing in Lexical using Yjs, a CRDT (Conflict-free Replicated Data Type) implementation.
Installation
npm install @lexical/yjs yjs
Yjs is a peer dependency. You must install it separately.
Core Concepts
Binding
A binding connects a Lexical editor to a Yjs shared type, synchronizing changes bidirectionally.
Provider
A provider handles network communication between clients (e.g., y-websocket, y-webrtc).
Awareness
Awareness tracks presence information like cursor positions and user metadata.
Creating a Binding
createBinding
Creates a binding between Lexical and Yjs.
function createBinding(
editor: LexicalEditor,
provider: Provider,
id: string,
doc: Doc,
docMap: Map<string, Doc>
): Binding
The Lexical editor instance
Yjs provider for network synchronization
Unique identifier for this document
Map of document IDs to Yjs documents
Returns: Binding object
createBindingV2__EXPERIMENTAL
Experimental V2 binding with improved performance.
function createBindingV2__EXPERIMENTAL(
editor: LexicalEditor,
provider: Provider,
id: string,
doc: Doc,
docMap: Map<string, Doc>,
excludedProperties?: Set<ExcludedProperties>
): BindingV2
Sync Functions
syncLexicalUpdateToYjs
Synchronizes Lexical changes to Yjs.
function syncLexicalUpdateToYjs(
binding: Binding,
provider: Provider,
prevEditorState: EditorState,
currEditorState: EditorState,
dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
dirtyLeaves: Set<NodeKey>,
normalizedNodes: Set<NodeKey>,
tags: Set<string>
): void
syncYjsChangesToLexical
Synchronizes Yjs changes to Lexical.
function syncYjsChangesToLexical(
binding: Binding,
provider: Provider,
events: Array<YjsEvent>,
isFromUndoManger: boolean
): void
Cursor Synchronization
syncCursorPositions
Synchronizes cursor positions across collaborators.
function syncCursorPositions(
binding: Binding,
provider: Provider
): void
getAnchorAndFocusCollabNodesForUserState
Gets collab nodes for a user’s cursor position.
function getAnchorAndFocusCollabNodesForUserState(
userState: UserState,
binding: Binding
): [CollabElementNode | null, CollabElementNode | null]
Undo Manager
createUndoManager
Creates a Yjs undo manager for collaborative undo/redo.
function createUndoManager(
binding: BaseBinding,
root: XmlText | XmlElement
): UndoManager
root
XmlText | XmlElement
required
Root Yjs XML element
Returns: Yjs UndoManager instance
Awareness
initLocalState
Initializes local user state in awareness.
function initLocalState(
provider: Provider,
name: string,
color: string,
focusing: boolean,
awarenessData: object
): void
User’s cursor/selection color
Whether user is currently focused on editor
setLocalStateFocus
Updates the focus state of the local user.
function setLocalStateFocus(
provider: Provider,
name: string,
color: string,
focusing: boolean,
awarenessData: object
): void
Commands
CONNECTED_COMMAND
Dispatched when connection state changes.
const CONNECTED_COMMAND: LexicalCommand<boolean>
Example:
import { CONNECTED_COMMAND } from '@lexical/yjs';
editor.registerCommand(
CONNECTED_COMMAND,
(isConnected: boolean) => {
console.log(isConnected ? 'Connected' : 'Disconnected');
return false;
},
COMMAND_PRIORITY_LOW
);
TOGGLE_CONNECT_COMMAND
Toggles connection to collaboration server.
const TOGGLE_CONNECT_COMMAND: LexicalCommand<boolean>
Version Diffing (Experimental)
DIFF_VERSIONS_COMMAND__EXPERIMENTAL
Compares two Yjs snapshots.
const DIFF_VERSIONS_COMMAND__EXPERIMENTAL: LexicalCommand<{
prevSnapshot?: Snapshot;
snapshot?: Snapshot;
}>
CLEAR_DIFF_VERSIONS_COMMAND__EXPERIMENTAL
Clears version diff display.
const CLEAR_DIFF_VERSIONS_COMMAND__EXPERIMENTAL: LexicalCommand<void>
Types
Provider
Interface for Yjs providers.
interface Provider {
awareness: ProviderAwareness;
connect(): void | Promise<void>;
disconnect(): void;
on(type: 'sync', cb: (isSynced: boolean) => void): void;
on(type: 'status', cb: (arg0: { status: string }) => void): void;
on(type: 'update', cb: (arg0: unknown) => void): void;
on(type: 'reload', cb: (doc: Doc) => void): void;
off(type: 'sync', cb: (isSynced: boolean) => void): void;
off(type: 'update', cb: (arg0: unknown) => void): void;
off(type: 'status', cb: (arg0: { status: string }) => void): void;
off(type: 'reload', cb: (doc: Doc) => void): void;
}
UserState
Represents a collaborating user’s state.
type UserState = {
anchorPos: null | RelativePosition;
color: string;
focusing: boolean;
focusPos: null | RelativePosition;
name: string;
awarenessData: object;
[key: string]: unknown;
}
Binding
Binding between Lexical and Yjs.
type Binding = {
clientID: number;
collabNodeMap: Map<NodeKey, CollabElementNode>;
doc: Doc;
docMap: Map<string, Doc>;
id: string;
nodeProperties: Map<string, NodeProperty>;
root: CollabElementNode;
}
Complete Example
import { createEditor } from 'lexical';
import { createBinding, initLocalState, CONNECTED_COMMAND } from '@lexical/yjs';
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
// Create Yjs document
const doc = new Y.Doc();
const docMap = new Map([['main', doc]]);
// Create WebSocket provider
const provider = new WebsocketProvider(
'ws://localhost:1234',
'my-document-id',
doc
);
// Create Lexical editor
const editor = createEditor({
namespace: 'CollaborativeEditor',
nodes: [/* your nodes */],
onError: (error) => console.error(error)
});
editor.setRootElement(document.getElementById('editor'));
// Create binding
const binding = createBinding(
editor,
provider,
'main',
doc,
docMap
);
// Initialize local user state
initLocalState(
provider,
'User ' + Math.floor(Math.random() * 1000),
'#' + Math.floor(Math.random() * 16777215).toString(16),
true,
{}
);
// Handle connection state
editor.registerCommand(
CONNECTED_COMMAND,
(isConnected: boolean) => {
const indicator = document.getElementById('connection-status');
indicator.textContent = isConnected ? 'Connected' : 'Disconnected';
indicator.className = isConnected ? 'connected' : 'disconnected';
return false;
},
COMMAND_PRIORITY_LOW
);
// Connect to server
provider.connect();
// Cleanup on unmount
function cleanup() {
provider.disconnect();
binding.root.destroy();
}
With Collaborative Cursors
import {
createBinding,
syncCursorPositions,
initLocalState,
setLocalStateFocus
} from '@lexical/yjs';
const binding = createBinding(editor, provider, 'main', doc, docMap);
// Initialize with user info
const userName = 'Alice';
const userColor = '#3b82f6';
initLocalState(provider, userName, userColor, true, {
avatar: 'https://example.com/avatar.jpg'
});
// Sync cursors on selection change
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
syncCursorPositions(binding, provider);
});
});
// Update focus state
window.addEventListener('focus', () => {
setLocalStateFocus(provider, userName, userColor, true, {});
});
window.addEventListener('blur', () => {
setLocalStateFocus(provider, userName, userColor, false, {});
});
// Render other users' cursors
provider.awareness.on('update', () => {
const states = provider.awareness.getStates();
states.forEach((state, clientID) => {
if (clientID !== binding.clientID) {
// Render cursor for this user
console.log(`User ${state.name} at`, state.anchorPos);
}
});
});
Server Setup Example
// server.js (Node.js)
const WebSocket = require('ws');
const { setupWSConnection } = require('y-websocket/bin/utils');
const wss = new WebSocket.Server({ port: 1234 });
wss.on('connection', (ws, req) => {
setupWSConnection(ws, req);
});
console.log('Collaboration server running on ws://localhost:1234');