Concepts / How content lives in Yjs
How content lives in Yjs
The whole note is one Yjs node, lexical-y. Text stays text. Each line ends in a ¶ marker that carries the line's type and fields, and blocks that aren't lines, like tables, become nested nodes. Type in the editor and watch the root change; hover a line or a row to pair them.
Reading the tree
- The root's attributes:
lexical-y:1=truerecords the layout version the note needs, andk:<type>records how each element type is stored, on its first write. - Text carries its formats:
f:bold,f:italic. A link is a format wrapper,w:link, whose value holds the link's fields. - A ¶ marker ends a line:
tis its type (left out for a paragraph) andp:<field>its schema fields, like a heading'sp:tag. - List items are lines in a group:
gnames the group's type,g:listTypeits identity, andlthe item's level. b:<type>nodes hold blocks that aren't lines, such as a table, its rows and its cells, each with lines of its own. Try typing-or##at the start of a line.
Why lines, not nodes
Typing, splitting and joining lines are text edits: Enter inserts a marker, and Backspace at the start of a line deletes one. Yjs merges them the way it merges any text, so two people splitting the same paragraph at once both keep their text. Indenting a list item changes its marker's level, not its place in a tree.
Storage kinds
@lexical/y derives how each element type is stored from the node structure its class declares in $config(), first match wins: a block with inline children is a line; an inline element with mergeAdjacent is a format; a block whose children are only lines, with mergeAdjacent, is a group; anything else is nested. The first three also need everything the class stores to be in its schema and NodeState, since a marker or a format can only carry those. A class that doesn't qualify falls back to nested, which stores the node as its own Yjs node and is always safe. describeStorage() prints the result for your editor.
import { describeStorage } from '@lexical/y';
import type * as Y from '@y/y';
import type { LexicalEditor } from 'lexical';
export function logStorage(editor: LexicalEditor, doc: Y.Doc) {
// How each node type is stored: 'line', 'group', 'format', 'nested' or 'text'.
console.table(describeStorage(editor));
// The note itself: text, ¶ line markers and nested nodes, with attributes.
const root = doc.get('lexical-y');
console.log(JSON.stringify(root.toDelta({ deep: true }), null, 2));
}These names are frozen as layout 1. Changing one makes a new layout version, which older clients open read only. The full wire format is in the @lexical/y README.