Skip to main content

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
editor
LexicalEditor
required
The Lexical editor instance
provider
Provider
required
Yjs provider for network synchronization
id
string
required
Unique identifier for this document
doc
Doc
required
Yjs document instance
docMap
Map<string, Doc>
required
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
binding
BaseBinding
required
The Yjs binding
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
provider
Provider
required
Yjs provider
name
string
required
User’s display name
color
string
required
User’s cursor/selection color
focusing
boolean
required
Whether user is currently focused on editor
awarenessData
object
required
Additional user metadata

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');

Build docs developers (and LLMs) love