Sign in

@liveblocks/lexical

@liveblocks/lexical provides React plugins that sync a Lexical editor with a Storage document tree and display remote carets and selections. Read our React or Next.js get started guides to learn more.

This package uses Liveblocks Storage. For Comments, mentions, and the full Text Editor product, use @liveblocks/react-lexical instead.

Setup

Install Liveblocks, Lexical, and this package:

Terminal
npm install @liveblocks/client @liveblocks/react @liveblocks/lexical lexical @lexical/react @lexical/selection @lexical/utils

Each Liveblocks package should use the same version.

Create a room with a root Storage document and an initial presence shape for selection cursors. The document is a LiveObject tree whose text leaves use LiveText:

liveblocks.config.ts
import type { LiveLexicalSelection, LiveRootNode } from "@liveblocks/lexical";
declare global { interface Liveblocks { Presence: { selection: LiveLexicalSelection | null; }; Storage: { document: LiveRootNode; }; UserMeta: { id?: string; info?: { name?: string; color?: string; }; }; }}
export {};
App.tsx
"use client";
import { LiveList, LiveObject, LiveText } from "@liveblocks/client";import { ClientSideSuspense, LiveblocksProvider, RoomProvider,} from "@liveblocks/react/suspense";import { Editor } from "./Editor";
export default function App() { return ( <LiveblocksProvider publicApiKey={""}> <RoomProvider id="my-room" initialPresence={{ selection: null }} initialStorage={{ document: new LiveObject({ kind: "root", type: "root", version: 1, children: new LiveList([ new LiveObject({ kind: "element", type: "paragraph", version: 1, children: new LiveList([ new LiveObject({ kind: "text", type: "text", version: 1, content: new LiveText(), }), ]), }), ]), }), }} > <ClientSideSuspense fallback={<div>Loading…</div>}> <Editor /> </ClientSideSuspense> </RoomProvider> </LiveblocksProvider> );}

Wait for Storage to load, then nest LiveblocksCollaborationPlugin inside LexicalComposer. Optionally add RemoteCursorsPlugin as a child to show remote carets:

Editor.tsx
"use client";
import { useCallback, useSyncExternalStore } from "react";import { LexicalComposer } from "@lexical/react/LexicalComposer";import { ContentEditable } from "@lexical/react/LexicalContentEditable";import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";import { LiveblocksCollaborationPlugin, RemoteCursorsPlugin,} from "@liveblocks/lexical";import type { Room } from "@liveblocks/client";import { useRoom } from "@liveblocks/react/suspense";import "@liveblocks/lexical/styles.css";
export function Editor() { const room = useRoom(); const root = useRoot(room);
if (root === null) { return <div>Loading…</div>; }
const document = root.get("document");
return ( <LexicalComposer initialConfig={{ namespace: "Liveblocks", onError: (error) => console.error(error), }} > <div className="relative"> <RichTextPlugin contentEditable={<ContentEditable className="outline-none" />} ErrorBoundary={LexicalErrorBoundary} /> <LiveblocksCollaborationPlugin root={document}> <RemoteCursorsPlugin /> </LiveblocksCollaborationPlugin> </div> </LexicalComposer> );}
function useRoot(room: Room) { const subscribe = room.events.storageDidLoad.subscribeOnce; const getSnapshot = room.getStorageOrNull; const getServerSnapshot = useCallback(() => null, []); return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);}

Import the package stylesheet so remote carets and selections are visible:

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

LiveblocksCollaborationPlugin

Syncs the Lexical editor with a Storage LiveRootNode. Local edits are written to Storage. Remote edits are applied to the editor. Undo and redo use the room’s history.

Must be nested inside LexicalComposer and a Liveblocks RoomProvider. Pass the Storage root document as root.

import { LiveblocksCollaborationPlugin } from "@liveblocks/lexical";
<LiveblocksCollaborationPlugin root={document}> <RemoteCursorsPlugin /></LiveblocksCollaborationPlugin>
Props
  • rootLiveRootNodeRequired

    The Storage root document for the editor. Typically root.get("document") after Storage has loaded.

  • childrenReactNode

    Optional children. Place RemoteCursorsPlugin here to render remote carets and selections.

RemoteCursorsPlugin

Renders remote carets and selection highlights for other users in the room. Must be a child of LiveblocksCollaborationPlugin.

Caret colors come from each user’s user.info.color. Set user info when authenticating or joining a room.

import {  LiveblocksCollaborationPlugin,  RemoteCursorsPlugin,} from "@liveblocks/lexical";
<LiveblocksCollaborationPlugin root={document}> <RemoteCursorsPlugin /></LiveblocksCollaborationPlugin>

Import @liveblocks/lexical/styles.css for the default cursor styles. The plugin uses --lb-lexical-cursor-color and the class names .lb-lexical-cursor-caret and .lb-lexical-cursor-selection.

LiveRootNode

The Storage type for the collaborative document root. It is a LiveObject with kind: "root", a children LiveList, and nested element, text, linebreak, and decorator nodes. Text leaves store content in LiveText.

import type { LiveRootNode } from "@liveblocks/lexical";import { LiveList, LiveObject, LiveText } from "@liveblocks/client";
const document: LiveRootNode = new LiveObject({ kind: "root", type: "root", version: 1, children: new LiveList([ new LiveObject({ kind: "element", type: "paragraph", version: 1, children: new LiveList([ new LiveObject({ kind: "text", type: "text", version: 1, content: new LiveText(), }), ]), }), ]),});

LiveLexicalSelection

The presence selection shape used by this package. Positions are Storage- relative (stable LiveObject ids and offsets), not Lexical node keys, so remote carets stay stable across concurrent edits.

import type { LiveLexicalSelection } from "@liveblocks/lexical";
type Presence = { selection: LiveLexicalSelection | null;};
Properties
  • anchorLiveLexicalPointRequired

    Selection anchor point in Storage coordinates.

  • focusLiveLexicalPointRequired

    Selection focus point in Storage coordinates.

Typing

Type your room’s presence, Storage, and user metadata in liveblocks.config.ts. Use LiveLexicalSelection for the presence selection field and LiveRootNode for Storage.

liveblocks.config.ts
import type { LiveLexicalSelection, LiveRootNode } from "@liveblocks/lexical";
declare global { interface Liveblocks { Presence: { selection: LiveLexicalSelection | null; }; Storage: { document: LiveRootNode; }; UserMeta: { id?: string; info?: { name?: string; color?: string; }; }; }}
export {};

When joining a room, set initialPresence to { selection: null } and initialStorage to a root document tree as shown in Setup.