Sign in

@liveblocks/prosemirror

@liveblocks/prosemirror provides ProseMirror plugins that sync editor documents with Liveblocks Storage and display remote carets and selections. Text nodes are stored as LiveText, preserving text formatting and concurrent edits.

If you are using Tiptap, use @liveblocks/react-tiptap with collaborationMode: "liveblocks" instead. It builds on this package and provides a Tiptap extension and React components.

This package is for client-side ProseMirror editors backed by Liveblocks Storage. For server-side editing of existing Tiptap and BlockNote documents, use @liveblocks/node-prosemirror.

Setup

Install Liveblocks and the ProseMirror packages used by your editor:

Terminal
npm install @liveblocks/client @liveblocks/react @liveblocks/prosemirror prosemirror-model prosemirror-state prosemirror-view

Each Liveblocks package should use the same version.

Add the collaboration plugin to sync the document and the caret plugin to show other users’ selections. The collaboration plugin creates its Storage document when it first loads, so initialStorage can remain empty.

Editor.tsx
"use client";
import { useEffect, useRef } from "react";import { createLiveblocksCollaborationCaretPlugin, createLiveblocksCollaborationPlugin,} from "@liveblocks/prosemirror";import "@liveblocks/prosemirror/styles.css";import { useRoom } from "@liveblocks/react/suspense";import { EditorState } from "prosemirror-state";import type { Schema } from "prosemirror-model";import { EditorView } from "prosemirror-view";
const INITIAL_CONTENT = { type: "doc", content: [ { type: "paragraph", content: [{ type: "text", text: "Hello world" }], }, ],};
export function Editor({ schema }: { schema: Schema }) { const room = useRoom(); const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => { if (containerRef.current === null) return;
const info = room.getSelf()?.info; const user = { name: typeof info?.name === "string" ? info.name : undefined, color: typeof info?.color === "string" ? info.color : undefined, }; const caretStorage = { users: [] };
const state = EditorState.create({ schema, plugins: [ createLiveblocksCollaborationPlugin({ room, field: "document", initialContent: INITIAL_CONTENT, fallbackDocument: () => INITIAL_CONTENT, }), createLiveblocksCollaborationCaretPlugin( { room, field: "document", user }, caretStorage ), ], });
const view = new EditorView(containerRef.current, { state }); return () => view.destroy(); }, [room, schema]);
return <div ref={containerRef} className="editor" />;}

The field value identifies the editor document. Documents are stored under root._tiptap_docs, keyed by field, so use a different value for each editor in the same room.

Import the package stylesheet to display remote carets and selections:

import "@liveblocks/prosemirror/styles.css";

createLiveblocksCollaborationPlugin

Creates a ProseMirror plugin that keeps an editor document in sync with Liveblocks Storage. Local transactions update the Storage document, while remote Storage changes are applied to the editor. Text leaves are represented by LiveText.

import { createLiveblocksCollaborationPlugin } from "@liveblocks/prosemirror";
const collaborationPlugin = createLiveblocksCollaborationPlugin({ room, field: "document", initialContent: { type: "doc", content: [{ type: "paragraph" }], },});

The plugin groups local edits into the room’s undo and redo history. Connect your editor’s undo and redo controls to room.history.undo() and room.history.redo().

Returns
  • pluginPlugin

    A ProseMirror plugin to add when creating the editor state.

Options
  • roomLiveblocksProsemirrorRoomRequired

    The Liveblocks room, retrieved with useRoom or client.enterRoom.

  • fieldstringRequired

    The name used to store this editor under root._tiptap_docs. Use a unique field for each editor in a room.

  • initialContentProseMirrorJsonNode

    The initial ProseMirror JSON document. It is used only when the Storage document does not exist. If omitted, the editor’s current document is used.

  • fallbackDocument() => ProseMirrorJsonNode

    Returns a schema-valid document if stored content cannot be parsed or is empty.

createLiveblocksCollaborationCaretPlugin

Creates a ProseMirror plugin that broadcasts the local selection through Presence and renders other users’ carets and selection highlights.

import {  createLiveblocksCollaborationCaretPlugin,  type CollaborationCaretStorage,} from "@liveblocks/prosemirror";
const storage: CollaborationCaretStorage = { users: [] };const caretPlugin = createLiveblocksCollaborationCaretPlugin( { room, field: "document", user: { name: "Ada", color: "#D583F0" }, }, storage);

The plugin renders elements with the .collaboration-carets__caret, .collaboration-carets__label, and .collaboration-carets__selection class names. The user’s color is applied with inline styles.

Returns
  • pluginPlugin

    A ProseMirror plugin to add when creating the editor state.

Arguments
  • options.roomLiveblocksProsemirrorRoomRequired

    The same Liveblocks room passed to createLiveblocksCollaborationPlugin.

  • options.fieldstringRequired

    The same document field passed to the collaboration plugin. Cursors from other fields are ignored.

  • options.userCursorUserRequired

    The name and color displayed with this user’s remote caret.

  • storageCollaborationCaretStorageRequired

    A mutable object with a users array. The plugin updates the array with the other users currently in the room.

Caret utilities

Use presencePatch when building a wrapper around the caret plugin or updating its user data outside the plugin. It creates the Presence update expected by other @liveblocks/prosemirror clients.

import { getCursorUser, presencePatch } from "@liveblocks/prosemirror";
const user = getCursorUser(room.getSelf()?.info) ?? {};
room.updatePresence( presencePatch({ field: "document", anchor: editorState.selection.anchor, head: editorState.selection.head, user, }));
Functions
  • presencePatch(presence) => JsonObject

    Creates a Presence patch containing the document field, selection positions, and optional cursor user.

  • getCursorUser(value: unknown) => CursorUser | undefined

    Reads string name and color properties from an unknown value.

LIVEBLOCKS_CARET_PRESENCE_KEY contains the Presence key used by the caret plugin. In most applications, the plugin manages this Presence value directly.

Plugin state

LIVEBLOCKS_COLLABORATION_PLUGIN_KEY

The key for reading the collaboration plugin state. isReady becomes true after Storage has loaded and the editor has received its initial document.

import { LIVEBLOCKS_COLLABORATION_PLUGIN_KEY } from "@liveblocks/prosemirror";
const { isReady } = LIVEBLOCKS_COLLABORATION_PLUGIN_KEY.getState( editorState) ?? { isReady: false };
State
  • isReadybooleanRequired

    Whether the initial Storage document has been loaded into the editor.

LIVEBLOCKS_CARET_PLUGIN_KEY

The key for reading the collaboration caret plugin state.

import { LIVEBLOCKS_CARET_PLUGIN_KEY } from "@liveblocks/prosemirror";
const state = LIVEBLOCKS_CARET_PLUGIN_KEY.getState(editorState);const remoteCursors = state?.cursors ?? [];
State
  • cursorsRemoteCursor[]Required

    The current remote cursor positions and user data.

  • decorationsDecorationSetRequired

    The ProseMirror decorations rendered for remote carets and selections.

Types

ProseMirrorJsonNode

The JSON representation accepted by the collaboration and conversion APIs.

Properties
  • typestringRequired

    The ProseMirror node type.

  • attrsJsonObject

    The node’s attributes.

  • contentProseMirrorJsonNode[]

    The node’s children.

  • textstring

    The content of a text node.

  • marksProseMirrorJsonMark[]

    The marks applied to a text node. Marks are stored as LiveText attributes.

CursorUser

The user information shown with a remote caret.

Properties
  • namestring

    The user’s display name. Defaults to "Anonymous" when rendered.

  • colorstring

    A CSS color for the user’s caret, label, and selection. Defaults to "#0f83ff".

RemoteCursor

The cursor data exposed by LIVEBLOCKS_CARET_PLUGIN_KEY.

Properties
  • anchornumberRequired

    The current mapped selection anchor.

  • headnumberRequired

    The current mapped selection head.

  • connectionIdnumberRequired

    The Liveblocks connection ID for the remote user.

  • rawAnchornumberRequired

    The most recent anchor received through Presence.

  • rawHeadnumberRequired

    The most recent head received through Presence.

  • userCursorUser

    The remote user’s display information.

Document conversion

The collaboration plugin automatically converts between ProseMirror JSON and a Liveblocks Storage tree. Use these helpers only when you need to inspect or construct that Storage representation directly.

createLiveblocksProsemirrorNode

Converts a ProseMirror JSON node into a LiveObject tree. Child nodes are stored in LiveList instances and text leaves are stored in LiveText.

import { createLiveblocksProsemirrorNode } from "@liveblocks/prosemirror";
const document = createLiveblocksProsemirrorNode({ type: "doc", content: [ { type: "paragraph", content: [{ type: "text", text: "Hello world" }], }, ],});
Returns
  • nodeLiveblocksProsemirrorNode

    The root of the converted Storage tree.

Arguments
  • nodeProseMirrorJsonNodeRequired

    The ProseMirror JSON node to convert.

liveblocksProsemirrorNodeToJson

Converts a LiveblocksProsemirrorNode Storage tree back to one ProseMirror JSON node.

import { liveblocksProsemirrorNodeToJson } from "@liveblocks/prosemirror";
const json = liveblocksProsemirrorNodeToJson(document, () => ({ type: "doc", content: [{ type: "paragraph" }],}));
Returns
  • nodeProseMirrorJsonNode

    The converted ProseMirror JSON node.

Arguments
  • nodeLiveblocksProsemirrorNodeRequired

    The Storage node to convert.

  • fallbackDocument() => ProseMirrorJsonNode

    Returns a document when the converted root document is empty.

liveblocksProsemirrorNodeToJsonNodes

Converts a LiveblocksProsemirrorNode to an array of ProseMirror JSON nodes. Formatted LiveText segments can produce multiple adjacent text nodes.

import { liveblocksProsemirrorNodeToJsonNodes } from "@liveblocks/prosemirror";
const nodes = liveblocksProsemirrorNodeToJsonNodes(document);
Returns
  • nodesProseMirrorJsonNode[]

    The converted ProseMirror JSON nodes.

Arguments
  • nodeLiveblocksProsemirrorNodeRequired

    The Storage node to convert.

Storage node helpers

Use these helpers to read values from a LiveblocksProsemirrorNode.

import {  getLiveblocksNodeContent,  getLiveblocksNodeId,  getLiveblocksNodeText,} from "@liveblocks/prosemirror";
const id = getLiveblocksNodeId(node);const children = getLiveblocksNodeContent(node);const text = getLiveblocksNodeText(node);
Functions
  • getLiveblocksNodeId(node) => string

    Returns the stable ID assigned to the Storage node.

  • getLiveblocksNodeContent(node) => LiveList<LiveblocksProsemirrorNode> | undefined

    Returns the child-node list for a non-text node.

  • getLiveblocksNodeText(node) => LiveText | undefined

    Returns the LiveText content for a text node.