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:
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
}- 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
Creating 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