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 Editor<TExtensions extends readonly unknown[] = []> {
api: Readonly<InstalledApiGroups<TExtensions>>;
getApi(extension: EditorExtension): unknown;
read<T>(fn: (state: EditorState) => T): T;
subscribe(listener: SnapshotListener): () => void;
subscribeCommit(listener: (commit: EditorCommit) => void): () => void;
update(
fn: (tx: EditorTransaction, context: EditorUpdateContext) => void,
options?: EditorUpdateOptions
): void;
extend(extension: EditorExtension | EditorExtension[]): () => void;
}interface Editor<TExtensions extends readonly unknown[] = []> {
api: Readonly<InstalledApiGroups<TExtensions>>;
getApi(extension: EditorExtension): unknown;
read<T>(fn: (state: EditorState) => T): T;
subscribe(listener: SnapshotListener): () => void;
subscribeCommit(listener: (commit: EditorCommit) => void): () => void;
update(
fn: (tx: EditorTransaction, context: EditorUpdateContext) => void,
options?: EditorUpdateOptions
): void;
extend(extension: EditorExtension | EditorExtension[]): () => void;
}On This Page
- Creating an editor
- Reading state
- Updating state
- Document roots
- Document state
- 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" }] }],
});const editor = createEditor({
initialValue: [{ type: "paragraph", children: [{ text: "Body" }] }],
});Extensions define schema, normalizers, commit listeners, operation middleware, feature namespaces, and optional runtime registration.
Reading state
editor.read(fn) => T
Read a coherent snapshot of editor state.
const selection = editor.read((state) => state.selection.get());const selection = editor.read((state) => state.selection.get());Use state for editor-state queries:
editor.read((state) => {
const children = state.nodes.children();
const marks = state.marks.get();
const first = state.nodes.get([0]);
const start = state.points.start([]);
const range = state.ranges.get([]);
return { children, first, marks, range, start };
});editor.read((state) => {
const children = state.nodes.children();
const marks = state.marks.get();
const first = state.nodes.get([0]);
const start = state.points.start([]);
const range = state.ranges.get([]);
return { children, first, marks, range, start };
});Schema policy is read through state.schema:
const isInline = editor.read((state) => state.schema.isInline(element));const isInline = editor.read((state) => state.schema.isInline(element));Updating state
editor.update(fn, options?) => void
Run a transaction. The callback receives tx, which owns command reads and
writes.
editor.update((tx) => {
tx.marks.toggle("bold");
});editor.update((tx) => {
tx.marks.toggle("bold");
});Use transaction groups for document changes:
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 });
});Replay operations through the transaction boundary:
editor.update(
(tx) => {
tx.operations.replay(remoteOperations);
},
{
tag: ["collaboration", "remote-import"],
metadata: {
collab: { origin: "remote", saveToHistory: false },
history: { mode: "skip" },
selection: { dom: "preserve" },
},
}
);editor.update(
(tx) => {
tx.operations.replay(remoteOperations);
},
{
tag: ["collaboration", "remote-import"],
metadata: {
collab: { origin: "remote", saveToHistory: false },
history: { mode: "skip" },
selection: { dom: "preserve" },
},
}
);tag is the cheap lifecycle label. metadata is the typed policy channel for
history, collaboration, and model/DOM selection behavior.
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", change.source);
});
});editor.update((tx, { afterCommit }) => {
tx.text.insert("Saved");
afterCommit((change) => {
analytics.track("editor-change", change.source);
});
});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 state.value.root(). Read an extra root by key.
const body = editor.read((state) => state.value.root());
const footer = editor.read((state) => state.value.root("footer"));const body = editor.read((state) => state.value.root());
const footer = editor.read((state) => state.value.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 state
state.value.get() returns the persisted document value.
type EditorDocumentValue = {
children: Descendant[];
roots?: Record<string, Descendant[]>;
state?: Record<string, unknown>;
};type EditorDocumentValue = {
children: Descendant[];
roots?: Record<string, Descendant[]>;
state?: Record<string, unknown>;
};Use it for database persistence because it includes the primary document, extra roots, and persistent state fields.
const documentValue = editor.read((state) => state.value.get());const documentValue = editor.read((state) => state.value.get());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(...).
editor.update(
(tx) => {
tx.statePatches.replay(remoteStatePatches);
},
{
metadata: {
collab: { origin: "remote", saveToHistory: false },
history: { mode: "skip" },
selection: { dom: "preserve" },
},
tag: ["collaboration", "remote-state"],
}
);editor.update(
(tx) => {
tx.statePatches.replay(remoteStatePatches);
},
{
metadata: {
collab: { origin: "remote", saveToHistory: false },
history: { mode: "skip" },
selection: { dom: "preserve" },
},
tag: ["collaboration", "remote-state"],
}
);See Document State 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/slate";
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/slate";
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 Slate 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.history.withoutSaving(() => {
editor.update((tx) => {
tx.text.insert("Imported");
});
});editor.api.dom.focus();
editor.api.clipboard.insertTextData(dataTransfer);
editor.api.history.withoutSaving(() => {
editor.update((tx) => {
tx.text.insert("Imported");
});
});Use api for services that are not transaction-scoped document mutations:
DOM/React bridges, clipboard ingress, history batching, mounted overlay handles,
measurements, or framework adapters. Do not put product editing commands there.
If a feature changes Slate 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((state) => state.value.get());
save(documentValue);
}
});const unsubscribe = editor.subscribe((_snapshot, change) => {
if (change?.childrenChanged || change?.dirtyStateKeys.length) {
const documentValue = editor.read((state) => state.value.get());
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. They should not add methods to
the editor object.
editor.update((tx) => {
tx.links.toggle({ href });
});editor.update((tx) => {
tx.links.toggle({ href });
});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(fn) => TUpdating stateeditor.update(fn, options?) => voidDocument rootsDocument stateSchema behaviorRuntime APIsSubscribing to commitseditor.subscribe(listener) => () => voideditor.subscribeCommit(listener) => () => voidExtending the editoreditor.extend(extension) => () => voidPure node and location helpers