An editor owns the document runtime. It is the root node of the document, and
its path is []. The public editor object is intentionally small.
Public Editor Shape
interface BaseEditor<
V extends Value = Value,
TExtensions extends readonly unknown[] = readonly [],
> {
readonly api: Readonly<
EditorCoreApiGroups & EditorInstalledApiGroups<TExtensions>
>;
readonly id: string;
extend(extension: EditorExtensionInput<any>): () => void;
getApi(extension: EditorExtension): unknown;
read: EditorRead<V, TExtensions>;
subscribe(listener: SnapshotListener): () => void;
subscribeCommit(listener: (commit: EditorCommit) => void): () => void;
update: EditorUpdate<V, TExtensions>;
}
type Editor<V extends Value = Value, TExtensions extends readonly unknown[] = readonly []> =
BaseEditor<V, TExtensions>;interface BaseEditor<
V extends Value = Value,
TExtensions extends readonly unknown[] = readonly [],
> {
readonly api: Readonly<
EditorCoreApiGroups & EditorInstalledApiGroups<TExtensions>
>;
readonly id: string;
extend(extension: EditorExtensionInput<any>): () => void;
getApi(extension: EditorExtension): unknown;
read: EditorRead<V, TExtensions>;
subscribe(listener: SnapshotListener): () => void;
subscribeCommit(listener: (commit: EditorCommit) => void): () => void;
update: EditorUpdate<V, TExtensions>;
}
type Editor<V extends Value = Value, TExtensions extends readonly unknown[] = readonly []> =
BaseEditor<V, TExtensions>;On This Page
- Creating an editor
- Reading state
- Locating nodes
- Updating state
- Document roots
- Document meta
- Schema behavior
- Runtime APIs
- Subscribing to commits
- Extending the editor
- Pure node and location helpers
Creating an editor
createEditor(options?) => Editor
Create an editor.
const editor = createEditor({
initialValue: [{ type: "paragraph", children: [{ text: "Body" }] }],
maxLength: 1000,
});const editor = createEditor({
initialValue: [{ type: "paragraph", children: [{ text: "Body" }] }],
maxLength: 1000,
});Extensions define schema, normalizers, commit listeners, operation middleware, feature namespaces, and optional runtime registration.
maxLength limits user-facing text, fragment, and node insertions. Operation
replay remains raw so imported operation logs keep their original contents.
Reading state
editor.read.<group>.<method>()
Run one committed-state read.
const selection = editor.read.selection();
const isExpanded = editor.read.selection.isExpanded();
const spansBlocks = editor.read.selection.isAcrossBlocks();
const startsBlock = editor.read.selection.isAtBlockStart();
const containsTitle = editor.read.selection.contains([0]);
const endsWord = selection
? editor.read.points.isWordEnd(selection.anchor)
: false;
const text = editor.read.text.string([]);
const isInline = editor.read.schema.isInline(element);const selection = editor.read.selection();
const isExpanded = editor.read.selection.isExpanded();
const spansBlocks = editor.read.selection.isAcrossBlocks();
const startsBlock = editor.read.selection.isAtBlockStart();
const containsTitle = editor.read.selection.contains([0]);
const endsWord = selection
? editor.read.points.isWordEnd(selection.anchor)
: false;
const text = editor.read.text.string([]);
const isInline = editor.read.schema.isInline(element);Selection predicates accept explicit targets when a command should inspect a location other than the current selection.
const startsHeading = editor.read.selection.isAtBlockStart({
at: point,
match: { type: "heading" },
});const startsHeading = editor.read.selection.isAtBlockStart({
at: point,
match: { type: "heading" },
});editor.read(fn) => T
Read a coherent snapshot of editor state when several reads should share the same state view.
const selection = editor.read((state) => state.selection());const selection = editor.read((state) => state.selection());Use state for editor-state queries:
editor.read((state) => {
const children = state.nodes.children();
const marks = state.marks();
const first = state.nodes.get([0]);
const isCollapsed = state.selection.isCollapsed();
const start = state.points.start([]);
const range = state.ranges.get([]);
return { children, first, isCollapsed, marks, range, start };
});editor.read((state) => {
const children = state.nodes.children();
const marks = state.marks();
const first = state.nodes.get([0]);
const isCollapsed = state.selection.isCollapsed();
const start = state.points.start([]);
const range = state.ranges.get([]);
return { children, first, isCollapsed, marks, range, start };
});Schema policy is available through direct read methods for common one-shot
checks and through state.schema for grouped reads:
const direct = editor.read.schema.isInline(element);
const grouped = editor.read((state) => state.schema.isInline(element));const direct = editor.read.schema.isInline(element);
const grouped = editor.read((state) => state.schema.isInline(element));Locating nodes
Public read and update methods with an at option accept a NodeTarget: a
Path, Point, Range, or live descendant.
type NodeTarget<N extends Descendant = Descendant> = Location | N;type NodeTarget<N extends Descendant = Descendant> = Location | N;Resolve a node when the path itself matters:
const path = editor.read.nodes.path(element);const path = editor.read.nodes.path(element);The read returns undefined for an unresolved node. Resolution is scoped to
the editor root, so nodes from another editor or another root do not resolve.
Strict static helpers keep their non-optional contracts and treat a missing
result from query middleware as an internal invariant failure.
Node queries accept predicates and shallow property objects.
const callout = editor.read.nodes.find({ match: { type: "callout" } });
const tableNode = editor.read.nodes.find({
match: { type: ["table", "table_cell"] },
});const callout = editor.read.nodes.find({ match: { type: "callout" } });
const tableNode = editor.read.nodes.find({
match: { type: ["table", "table_cell"] },
});Property values use exact equality. An array means one-of. Use predicates for computed schema policy and static type narrowing.
Updating state
editor.update.<group>.<method>()
Run one transaction write with the default policy.
editor.update.marks.toggle("bold");
editor.update.text.insert("Title");
editor.update.selection.move({ distance: 1 });
editor.update.nodes.set({ icon: "🔥" }, { at: calloutElement });editor.update.marks.toggle("bold");
editor.update.text.insert("Title");
editor.update.selection.move({ distance: 1 });
editor.update.nodes.set({ icon: "🔥" }, { at: calloutElement });editor.update(policy).<group>.<method>()
Configure one direct write.
editor.update({ history: "skip" }).text.insert("Imported");
editor.update({ tags: ["paste", "html"] }).fragment.insert(fragment);editor.update({ history: "skip" }).text.insert("Imported");
editor.update({ tags: ["paste", "html"] }).fragment.insert(fragment);The configured facade exposes core and installed extension update methods. A
method marked with txOnly(...) is omitted because it requires an explicit
transaction. editor.update(policy) returns that configured facade; a direct
method returns the underlying method result.
editor.update(fn) => void
Run one atomic update with the default policy. The callback receives tx, which
owns reads and writes for the active transaction.
editor.update((tx) => {
tx.nodes.set({ type: "heading" });
tx.text.insert("Title");
tx.selection.move({ distance: 1 });
});editor.update((tx) => {
tx.nodes.set({ type: "heading" });
tx.text.insert("Title");
tx.selection.move({ distance: 1 });
});editor.update(policy, fn) => void
Run one atomic update with a semantic policy.
editor.update({ history: "new-batch", tags: "paste" }, (tx) => {
tx.fragment.insert(fragment);
tx.selection.collapse({ edge: "end" });
});editor.update({ history: "new-batch", tags: "paste" }, (tx) => {
tx.fragment.insert(fragment);
tx.selection.collapse({ edge: "end" });
});type EditorUpdatePolicy = Readonly<{
history?: "merge" | "new-batch" | "skip";
tags?: EditorUpdateTag | readonly EditorUpdateTag[];
}>;type EditorUpdatePolicy = Readonly<{
history?: "merge" | "new-batch" | "skip";
tags?: EditorUpdateTag | readonly EditorUpdateTag[];
}>;EditorUpdatePolicyFor<E> narrows the policy to an editor's installed
capabilities. history is a type error when E has no History transaction
group, and an untyped runtime call fails before mutation. Tags are applied in
input order before history; only the last history intent remains.
Inside the callback, tx.tags.add(tag) updates the final tag set and
tx.tags.has(tag) inspects it. History adds tx.history.skip(),
tx.history.merge(), and tx.history.newBatch() as transaction-only controls
for decisions made after the update starts.
The update callback also receives a context object for local post-commit hooks:
editor.update((tx, { afterCommit }) => {
tx.text.insert("Saved");
afterCommit((change) => {
analytics.track("editor-change", { tags: change.tags });
});
});editor.update((tx, { afterCommit }) => {
tx.text.insert("Saved");
afterCommit((change) => {
analytics.track("editor-change", { tags: change.tags });
});
});All callback forms return void, must finish synchronously, and invalidate
tx when they return. Thenable callbacks and escaped transactions are rejected.
A public update cannot be nested; pass the active tx into helpers instead.
Document roots
A plain block array initializes the primary document. Pass
initialValue.children plus initialValue.roots when one editor owns extra
roots.
const editor = createEditor({
initialValue: {
children: [{ type: "paragraph", children: [{ text: "Body" }] }],
roots: {
header: [{ type: "paragraph", children: [{ text: "Draft" }] }],
footer: [{ type: "paragraph", children: [{ text: "Internal" }] }],
},
},
});const editor = createEditor({
initialValue: {
children: [{ type: "paragraph", children: [{ text: "Body" }] }],
roots: {
header: [{ type: "paragraph", children: [{ text: "Draft" }] }],
footer: [{ type: "paragraph", children: [{ text: "Internal" }] }],
},
},
});Read the primary document with editor.read.children(). Read an extra root by key.
const body = editor.read.children();
const footer = editor.read.root("footer");const body = editor.read.children();
const footer = editor.read.root("footer");Create, replace, or delete extra roots with tx.roots.
editor.update((tx) => {
tx.roots.create("aside:1", [
{ type: "paragraph", children: [{ text: "Aside" }] },
]);
});editor.update((tx) => {
tx.roots.create("aside:1", [
{ type: "paragraph", children: [{ text: "Aside" }] },
]);
});Use normal node and text transforms for the primary document. See Roots for React rendering, root chrome, and content roots.
Document meta
editor.read.value() returns the persisted document value.
type EditorDocumentValue = {
children: Descendant[];
roots?: Record<string, Descendant[]>;
meta?: Record<string, unknown>;
};type EditorDocumentValue = {
children: Descendant[];
roots?: Record<string, Descendant[]>;
meta?: Record<string, unknown>;
};Use it for database persistence because it includes the primary document, extra roots, and persistent meta fields.
const documentValue = editor.read.value();const documentValue = editor.read.value();State fields are registered with defineStateField and read through
state.getField(field).
const title = editor.read((state) => state.getField(documentTitle));const title = editor.read((state) => state.getField(documentTitle));Write state fields with tx.setField.
editor.update((tx) => {
tx.setField(documentTitle, "Q3 Launch Brief");
});editor.update((tx) => {
tx.setField(documentTitle, "Q3 Launch Brief");
});State-field writes appear in commit.statePatches and
commit.dirtyStateKeys. Collaboration adapters should export only shared
state-patch keys. Replay remote state patches with tx.statePatches.replay(...).
Adapter packages own their import policy; @platejs/yjs exports
YjsUpdatePolicy.remote.
import { YjsUpdatePolicy } from "@platejs/yjs";
editor.update(YjsUpdatePolicy.remote, (tx) => {
tx.statePatches.replay(remoteStatePatches);
});import { YjsUpdatePolicy } from "@platejs/yjs";
editor.update(YjsUpdatePolicy.remote, (tx) => {
tx.statePatches.replay(remoteStatePatches);
});See Document Meta for persistence patterns and comments ownership.
Schema behavior
Schema setup belongs to extensions. Read schema policy through state.schema
or tx.schema.
import { defineEditorExtension, elementProperty } from "@platejs/plite";
const tables = defineEditorExtension({
name: "tables",
elements: [
{
type: "table-cell",
isolating: true,
keyboardSelectable: true,
properties: {
colSpan: elementProperty.number({ default: 1 }),
rowSpan: elementProperty.number({ default: 1 }),
},
},
],
});
editor.extend(tables);
editor.read((state) => state.schema.isVoid(element));
editor.update((tx) => {
if (tx.schema.isInline(element)) {
tx.selection.move({ unit: "character" });
}
});import { defineEditorExtension, elementProperty } from "@platejs/plite";
const tables = defineEditorExtension({
name: "tables",
elements: [
{
type: "table-cell",
isolating: true,
keyboardSelectable: true,
properties: {
colSpan: elementProperty.number({ default: 1 }),
rowSpan: elementProperty.number({ default: 1 }),
},
},
],
});
editor.extend(tables);
editor.read((state) => state.schema.isVoid(element));
editor.update((tx) => {
if (tx.schema.isInline(element)) {
tx.selection.move({ unit: "character" });
}
});Common schema checks include:
state.schema.getElementBehavior(element)state.schema.getElementProperty(element, property)state.schema.getElementPropertyDescriptor(type, property)state.schema.isAtom(element)state.schema.isEditableIsland(element)state.schema.isInline(element)state.schema.isIsolating(element)state.schema.isKeyboardSelectable(element)state.schema.isReadOnly(element)state.schema.isVoid(element)state.schema.markableVoid(element)state.schema.isSelectable(element)state.schema.isElementPropertyEqual(type, property, left, right)
Element property descriptors provide defaults and equality for extension-owned element fields. Reading a default does not write that property into the document. The Plite value remains plain JSON until your transaction writes a field.
Runtime APIs
Extensions expose mounted host and runtime services through editor.api.
editor.api.dom.focus();
editor.api.clipboard.insertTextData(dataTransfer);editor.api.dom.focus();
editor.api.clipboard.insertTextData(dataTransfer);Use api for services that are not transaction-scoped document mutations:
DOM/React bridges, clipboard ingress, mounted overlay handles, measurements, or
framework adapters. Do not put product editing commands there.
If a feature changes Plite model state, expose it as a tx group and call it
inside editor.update(...).
Use editor.getApi(extension) when the call site owns the extension token and
needs the typed API for that extension.
Subscribing to commits
editor.subscribe(listener) => () => void
Subscribe to editor snapshots. The listener receives the current snapshot and an optional change summary.
const unsubscribe = editor.subscribe((_snapshot, change) => {
if (change?.childrenChanged || change?.dirtyStateKeys.length) {
const documentValue = editor.read.value();
save(documentValue);
}
});const unsubscribe = editor.subscribe((_snapshot, change) => {
if (change?.childrenChanged || change?.dirtyStateKeys.length) {
const documentValue = editor.read.value();
save(documentValue);
}
});editor.subscribeCommit(listener) => () => void
Subscribe only to committed changes. The listener receives the change summary for each commit.
const unsubscribe = editor.subscribeCommit((change) => {
if (change.selectionChanged) {
syncSelection(change.selection);
}
});const unsubscribe = editor.subscribeCommit((change) => {
if (change.selectionChanged) {
syncSelection(change.selection);
}
});Call the returned function to unsubscribe.
Extending the editor
editor.extend(extension) => () => void
Install an extension and return a cleanup function.
const removeExtension = editor.extend(myExtension);const removeExtension = editor.extend(myExtension);Extensions add typed state and tx namespaces. Direct-safe transaction
methods are also available through the matching editor.update group.
editor.update.links.toggle({ href });
editor.update((tx) => {
tx.links.toggle({ href });
});editor.update.links.toggle({ href });
editor.update((tx) => {
tx.links.toggle({ href });
});Wrap a transaction-only extension method with txOnly(...). TypeScript omits
it from direct update groups, and dynamic direct dispatch rejects it at runtime.
Pure node and location helpers
Pure helpers stay on their own namespaces because they do not read editor runtime state.
NodeApi.string(node);
ElementApi.isElement(value);
TextApi.isText(value);
PathApi.next(path);
PointApi.equals(point, other);
RangeApi.isCollapsed(range);
OperationApi.isOperation(value);NodeApi.string(node);
ElementApi.isElement(value);
TextApi.isText(value);
PathApi.next(path);
PointApi.equals(point, other);
RangeApi.isCollapsed(range);
OperationApi.isOperation(value);Use editor.read(...) or editor.update(...) when a helper needs editor
state.
On This Page
Public Editor ShapeOn This PageCreating an editorcreateEditor(options?) => EditorReading stateeditor.read.<group>.<method>()editor.read(fn) => TLocating nodesUpdating stateeditor.update.<group>.<method>()editor.update(policy).<group>.<method>()editor.update(fn) => voideditor.update(policy, fn) => voidDocument rootsDocument metaSchema behaviorRuntime APIsSubscribing to commitseditor.subscribe(listener) => () => voideditor.subscribeCommit(listener) => () => voidExtending the editoreditor.extend(extension) => () => voidPure node and location helpers