From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Stream
    • Copilot
  • Discussion
    • Comments
    • Suggestion
    • Basic Blocks
      • Blockquote
      • Heading
      • Horizontal Rule
    • Callout
    • Code Block
    • Column
    • Date
    • Equation
    • Link
    • List Classic
    • Media
    • MentionElement
    • Table
    • Table of Contents
    • Footnote
    • Toggle
  • Marks
    • Bold
    • Italic
    • Underline
    • Code
    • Highlight
    • Keyboard Input
    • Strikethrough
    • Subscript
    • Superscript
      • Font
      • Line Height
      • Text Align
    • Indent
    • List
      • Exit Break
      • Single Block
      • Trailing Block
    • Autoformat
    • Block Menu
    • Block Placeholder
    • Combobox
      • Emoji
      • MentionElement
      • Slash Command
    • Cursor Overlay
    • Drag & Drop
    • Navigation Feedback
    • Tabbable
    • Toolbar
    • Yjs
    • Multi SelectEditor
    • CSV
    • DOCX
    • HTML
    • Markdown

Collaboration

PreviousNext

Real-time collaboration with Yjs

Loading…
ToolbarMulti Select

On This Page

FeaturesManual UsageInstallationAdd The PluginConfigure Provider And CursorsAdd Editor ContainerRead And Control Connection StateMonitor Connection Status (Optional)Provider BoundaryConnection CommandsBackend SetupHocuspocusWebRTCIndexedDBTroubleshootingRelated
Build your editor
Production-ready AI template and reusable components.
Get all-access

The preview runs two independent editors through a credential-free local room. It demonstrates remote document and cursor updates, disconnect/reconnect, schema rejection and recovery, and Plite History undo after a remote edit. Apps still own production transport, authentication, persistence, and room naming.

Features

  • Provider Boundary: Your app owns Hocuspocus, WebSocket, WebRTC, IndexedDB, or custom transport code. @platejs/yjs adapts a Y.Doc, provider lifecycle, awareness, selection state, and canonical editor changes.
  • Custom Providers: Wrap provider packages as YjsProviderLike and pass them to YjsPlugin.configure.
  • Awareness & Cursors: Integrates Yjs Awareness protocol for sharing cursor locations and other ephemeral state between users. Includes RemoteCursorOverlay for rendering remote cursors.
  • Multi-Root Documents: Synchronizes primary children, named roots, and root-qualified awareness through one editor extension and Yjs transaction boundary.
  • Customizable Cursors: Send cursor names, colors, and other metadata through tx.yjs.sendSelection.
  • Explicit Commands: The plugin installs the Plite Yjs extension, including state.yjs reads and tx.yjs commands for connection, awareness, and selection.
Report an issue

Manual Usage

Installation

Install the Yjs plugin.

pnpm add @platejs/yjs
pnpm add @platejs/yjs

For Hocuspocus server-based collaboration:

pnpm add @hocuspocus/provider
pnpm add @hocuspocus/provider

For WebRTC peer-to-peer collaboration:

pnpm add y-webrtc
pnpm add y-webrtc

Add The Plugin

Every peer in a room must use the same compiled schema identity. Derived schemas match by fingerprint. Use an explicit id and version for a durable room lineage, and migrate the room before bumping the version.

import { YjsPlugin } from "@platejs/yjs/plate";
import { createPlateEditor } from "platejs/react";
 
const editor = createPlateEditor({
  schema: { id: "yjs-example", version: 1 },
  plugins: [
    YjsPlugin.configure({
      initialState: {
        clientId: "local-user",
        doc,
        provider,
        rootName: roomId,
      },
    }),
  ],
  initialValue,
});
Provider Boundary

App code owns the provider package, room name, authentication, persistence, and server scaling. YjsPlugin owns Plate composition and installs the Plite/Yjs synchronization extension.

Configure Provider And Cursors

Wrap your provider as YjsProviderLike, then pass it through the plugin's initialState. The Plate subpath exports the plugin; the React subpath exports remote cursor hooks.

import { YjsPlugin } from "@platejs/yjs/plate";
import { useYjsRemoteCursors } from "@platejs/yjs/react";
import { RemoteCursorOverlay } from "@/components/editor/remote-cursor-overlay";
 
export const yjsPlugin = YjsPlugin.configure({
  initialState: {
    clientId: user.id,
    doc,
    provider,
    rootName: roomId,
  },
});
import { YjsPlugin } from










  • provider: A YjsProviderLike wrapper around app-owned transport code.
  • doc: The shared Y.Doc.
  • rootName: The collaboration document namespace. Its primary and named roots synchronize together.
  • Standalone collab: "shared" effect descriptors belong to installed editor extensions. YjsPlugin discovers them from the editor.
  • Cursor data is sent through tx.yjs.sendSelection(...) when the local selection changes.

One Plate commit that changes primary and named roots becomes one Yjs transaction. Plite History records that editor commit as one history batch. Named roots synchronize without a mounted PlateContent, and remote selections preserve their root key. A selection cannot span two roots.

Add Editor Container

The RemoteCursorOverlay requires a positioned container around the editor content. Use EditorContainer component or PlateContainer from platejs/react:

import { Plate } from "platejs/react";
import { EditorContainer } from "@/components/editor/editor";
 
return (
  <Plate editor={editor}>
    <EditorContainer>
      <Editor />
    </EditorContainer>
  </Plate>
);
import { Plate } from "platejs/react";
import { EditorContainer } from







Read And Control Connection State

The plugin installs a yjs group in editor.read and editor.update.

const connected = editor.read((state) => state.yjs.connected());
const providerStatus = editor.read((state) => state.yjs.providerStatus());
 
editor.update((tx) => {
  tx.yjs.connect();
  tx.yjs.sendSelection(selection, { name: "Ada", color: "#aabbcc" });
});






The installed extension cleanup destroys editor-owned listeners. Provider destruction depends on whether the provider wrapper declares itself editor-owned.

Monitor Connection Status (Optional)

Access provider states and add event handlers for connection monitoring:

import React from "react";
import { useYjsProviderStatus, useYjsProviderSynced } from "@platejs/yjs/react";
 
function EditorStatus() {
  const status = useYjsProviderStatus(editor);
  const synced = useYjsProviderSynced(editor);
 
  return (
    <div>
      {status} {synced ? "Synced" : "Syncing"}
    </div>
  );
}

Provider Boundary

Provider packages stay at the app edge. Wrap Hocuspocus, WebSocket, WebRTC, IndexedDB, or another transport as YjsProviderLike, then pass it through YjsPlugin.configure. The wrapper owns network connection state. The plugin installs the Plite/Yjs synchronization extension.

ConcernOwner
Y.Doc and root nameApp plus YjsPlugin config
Provider package installationApp
Room naming and authenticationApp
Persistence and server scalingApp
Editor operations, selection, undo/redo@platejs/plite and @platejs/plite-history
Awareness and remote cursors@platejs/yjs

Connection Commands

Use editor.read for provider state and editor.update for commands.

const connected = editor.read((state) => state.yjs.connected());
const synced = editor.read((state) => state.yjs.providerSynced());
 
editor.update((tx) => {
  tx.yjs.connect();
  tx.yjs.sendSelection(selection, { name: "Ada", color: "#aabbcc" });
});
const connected = editor.read((state) => state.yjs.connected());
const synced = editor.read((state) => state.yjs.providerSynced());
 
editor.update((tx) => {
  tx.yjs.connect();
  tx.yjs.sendSelection(selection, { name: "Ada", color: "#aabbcc" });
});
CommandUse
tx.yjs.connect()Connect the provider.
tx.yjs.disconnect()Disconnect without tearing down the editor.
tx.yjs.reconnect()Reconnect after a provider interruption.
tx.yjs.sendSelection(range, data)Publish local selection and cursor metadata.
tx.yjs.clearSelection()Clear local awareness selection.

Install Plite history for undo and redo. Its canonical replay flows through the same Yjs synchronization bridge as every other editor update:

editor.update.history.undo();
editor.update.history.redo();
editor.update.history.undo();
editor.update.history.redo();

Backend Setup

Hocuspocus

Install and configure @hocuspocus/provider in app code. Pass its shared Y.Doc and provider wrapper to YjsPlugin.configure. Keep auth tokens, room names, and server URL outside the Plate package.

WebRTC

Install y-webrtc in app code when peer-to-peer collaboration is a fit. Use your own signaling and TURN infrastructure for production. The editor package only needs the wrapped provider and shared Y.Doc.

IndexedDB

Use y-indexeddb as local app persistence when you want a document to restore before remote sync finishes. IndexedDB does not transport remote awareness or cursors by itself.

Troubleshooting

  • Verify every collaborator uses the same Y.Doc root name.
  • Keep provider connection errors visible in app logs.
  • Do not mutate the shared Y.Doc behind the editor while the editor is mounted.
  • Use tx.yjs.disconnect() for temporary disconnects and extension cleanup for unmount teardown.
  • If remote cursors are wrong, verify the app sends cursor metadata through tx.yjs.sendSelection(...) and renders the React cursor hooks from @platejs/yjs/react.

Related

  • Plite Yjs
  • Yjs
  • Hocuspocus
  • y-webrtc
  • y-indexeddb
  • RemoteCursorOverlay
  • EditorContainer
import { YjsPlugin } from "@platejs/yjs/plate";
import { createPlateEditor } from "platejs/react";
 
const editor = createPlateEditor({
  schema: { id: "yjs-example", version: 1 },
  plugins: [
    YjsPlugin.configure({
      initialState: {
        clientId: "local-user",
        doc,
        provider,
        rootName: roomId,
      },
    }),
  ],
  initialValue,
});
"@platejs/yjs/plate"
;
import { useYjsRemoteCursors } from "@platejs/yjs/react";
import { RemoteCursorOverlay } from "@/components/editor/remote-cursor-overlay";
export const yjsPlugin = YjsPlugin.configure({
initialState: {
clientId: user.id,
doc,
provider,
rootName: roomId,
},
});
"@/components/editor/editor"
;
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
const
connected
=
editor.
read
((
state
)
=>
state.yjs.
connected
());
const providerStatus = editor.read((state) => state.yjs.providerStatus());
editor.update((tx) => {
tx.yjs.connect();
tx.yjs.sendSelection(selection, { name: "Ada", color: "#aabbcc" });
});
import React from "react"; import { useYjsProviderStatus, useYjsProviderSynced } from "@platejs/yjs/react"; function EditorStatus() { const status = useYjsProviderStatus(editor); const synced = useYjsProviderSynced(editor); return ( <div> {status} {synced ? "Synced" : "Syncing"} </div> ); }