Skip to main content
Lexical supports real-time collaboration through the @lexical/yjs package, which integrates with Yjs for conflict-free replicated data types (CRDTs).

Overview

The collaboration system enables:
  • Multi-user editing with automatic conflict resolution
  • Cursor presence showing where other users are typing
  • Undo/Redo that works correctly with concurrent edits
  • Offline support with automatic sync when reconnected

Setup

1
Install Dependencies
2
npm install yjs @lexical/yjs
# Choose a provider (WebSocket, WebRTC, etc.)
npm install y-websocket
3
Create Yjs Document
4
import { Doc } from 'yjs';
import { WebsocketProvider } from 'y-websocket';

const doc = new Doc();
const provider = new WebsocketProvider(
  'ws://localhost:1234',
  'my-document-id',
  doc,
);
5
Create Binding
6
import { createBinding } from '@lexical/yjs';

const binding = createBinding(
  editor,
  provider,
  doc.getXmlFragment('root'),
  new Map(),
  undefined,
  'user-123',
);

React Integration

Use the CollaborationPlugin for React applications:
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { CollaborationPlugin } from '@lexical/react/LexicalCollaborationPlugin';
import { Provider } from '@lexical/yjs';
import { Doc } from 'yjs';
import { WebsocketProvider } from 'y-websocket';

function Editor() {
  const initialConfig = {
    namespace: 'CollaborativeEditor',
    onError: (error: Error) => console.error(error),
  };

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <RichTextPlugin
        contentEditable={<ContentEditable />}
        placeholder={<div>Start typing...</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
      <CollaborationPlugin
        id="my-document-id"
        providerFactory={(id, yjsDocMap) => {
          const doc = new Doc();
          yjsDocMap.set(id, doc);
          
          const provider = new WebsocketProvider(
            'ws://localhost:1234',
            id,
            doc,
          );
          
          return provider;
        }}
        shouldBootstrap={true}
      />
    </LexicalComposer>
  );
}
See packages/lexical-react/src/LexicalCollaborationPlugin.tsx for implementation details.

Cursor Presence

Show where other users are editing:
import { CollaborationPlugin } from '@lexical/react/LexicalCollaborationPlugin';

function Editor() {
  return (
    <LexicalComposer initialConfig={config}>
      <RichTextPlugin ... />
      <CollaborationPlugin
        id="doc-id"
        providerFactory={createProvider}
        shouldBootstrap={true}
        cursorsContainerRef={cursorsRef}
      />
    </LexicalComposer>
  );
}
Custom cursor styling:
.collaboration-cursor {
  position: absolute;
  pointer-events: none;
  border-left: 2px solid;
  height: 1.2em;
  transition: all 0.15s ease;
}

.collaboration-cursor-caret {
  position: absolute;
  top: -1.5em;
  left: -2px;
  padding: 2px 6px;
  border-radius: 3px;
  font-size: 12px;
  color: white;
  white-space: nowrap;
}

User Awareness

Track connected users:
import { initLocalState, setLocalStateFocus } from '@lexical/yjs';

const provider = new WebsocketProvider('ws://localhost:1234', 'doc-id', doc);

// Initialize user state
initLocalState(
  provider,
  'User Name',
  '#FF0000', // cursor color
  true,      // focusing
  {},        // additional awareness data
);

// Update focus state
setLocalStateFocus(
  provider,
  'User Name',
  '#FF0000',
  true, // focused
  {},
);

// Listen to awareness changes
provider.awareness.on('update', () => {
  const states = provider.awareness.getStates();
  states.forEach((state, clientId) => {
    console.log(`User ${state.name} is ${state.focusing ? 'active' : 'idle'}`);
  });
});
See packages/lexical-yjs/src/index.ts:99-152 for awareness implementation.

Undo/Redo with Collaboration

Create a collaborative undo manager:
import { createUndoManager } from '@lexical/yjs';
import { registerYjsUndo } from '@lexical/yjs';

const binding = createBinding(editor, provider, root, ...);
const undoManager = createUndoManager(binding, root);

// Use with history plugin
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';

<HistoryPlugin externalHistoryState={undoManager} />
This ensures undo/redo only affects the current user’s changes.

Sync Strategies

WebSocket Provider

Best for centralized server architecture:
import { WebsocketProvider } from 'y-websocket';

const provider = new WebsocketProvider(
  'ws://localhost:1234',
  'document-id',
  doc,
  {
    connect: true,
    WebSocketPolyfill: WebSocket,
  },
);

provider.on('status', (event: { status: string }) => {
  console.log('Connection status:', event.status);
});

provider.on('sync', (isSynced: boolean) => {
  console.log('Synced:', isSynced);
});

WebRTC Provider

Best for peer-to-peer without a central server:
npm install y-webrtc
import { WebrtcProvider } from 'y-webrtc';

const provider = new WebrtcProvider('document-id', doc, {
  signaling: ['wss://signaling.yjs.dev'],
  password: 'optional-room-password',
});

IndexedDB Provider

For offline persistence:
npm install y-indexeddb
import { IndexeddbPersistence } from 'y-indexeddb';

const persistence = new IndexeddbPersistence('document-id', doc);

persistence.on('synced', () => {
  console.log('Content loaded from IndexedDB');
});

Server Setup

Example WebSocket server using y-websocket:
// server.js
const WebSocket = require('ws');
const http = require('http');
const { setupWSConnection } = require('y-websocket/bin/utils');

const server = http.createServer();
const wss = new WebSocket.Server({ server });

wss.on('connection', (ws, req) => {
  setupWSConnection(ws, req);
});

server.listen(1234, () => {
  console.log('Collaboration server running on port 1234');
});
With persistence:
const { LeveldbPersistence } = require('y-leveldb');

const persistence = new LeveldbPersistence('./yjs-data');

wss.on('connection', (ws, req) => {
  setupWSConnection(ws, req, { persistence });
});
See the Lexical playground server: packages/lexical-playground/src/collaboration.ts

Conflict Resolution

Yjs automatically resolves conflicts using CRDTs:
// User A types "Hello"
// User B types "World" at the same position
// Result: "HelloWorld" or "WorldHello" (deterministic based on client IDs)
No manual conflict resolution needed!

Testing Collaboration

Test with multiple browser windows:
# Terminal 1: Start collab server
cd packages/lexical-playground
npm run collab

# Terminal 2: Start dev server
npm run dev

# Open http://localhost:3000 in multiple browser windows

Advanced Features

Presence Metadata

Share additional user data:
initLocalState(
  provider,
  'John Doe',
  '#3b82f6',
  true,
  {
    avatar: 'https://example.com/avatar.jpg',
    role: 'editor',
    lastActive: Date.now(),
  },
);

Custom Sync Logic

import {
  syncLexicalUpdateToYjs,
  syncYjsChangesToLexical,
} from '@lexical/yjs';

editor.registerUpdateListener(({ editorState }) => {
  syncLexicalUpdateToYjs(
    binding,
    provider,
    editorState,
  );
});

provider.on('update', () => {
  syncYjsChangesToLexical(binding, provider);
});

Version History

import { DIFF_VERSIONS_COMMAND__EXPERIMENTAL } from '@lexical/yjs';

const snapshot = doc.snapshot();

// Later, compare versions
editor.dispatchCommand(DIFF_VERSIONS_COMMAND__EXPERIMENTAL, {
  prevSnapshot: snapshot,
  snapshot: doc.snapshot(),
});
See packages/lexical-yjs/src/index.ts:34-47 for experimental version diff commands.

Best Practices

  • Unique User IDs: Assign unique IDs to each user
  • Connection Status: Show connection state to users
  • Offline Support: Use IndexedDB for offline persistence
  • Error Handling: Handle connection failures gracefully
  • Cleanup: Disconnect providers on unmount
  • Security: Add authentication to your WebSocket server
  • Scalability: Use a dedicated Yjs server for production

Troubleshooting

Content Not Syncing

  • Check WebSocket connection status
  • Verify document ID matches across clients
  • Ensure shouldBootstrap is set correctly

Cursor Positions Wrong

  • Verify all clients use the same node types
  • Check that custom nodes implement serialization correctly

Performance Issues

  • Limit awareness updates frequency
  • Use debouncing for cursor position updates
  • Consider server-side persistence for large documents

See Also

Build docs developers (and LLMs) love