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
}- 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
Create an editor.
const editor = createEditor({
initialValue: [{ type: 'paragraph', children: [{ text: 'Body' }] }],
})Extensions define schema, normalizers, commit listeners, operation middleware, feature namespaces, and optional runtime registration.
Read a coherent snapshot of editor state.
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 }
})Schema policy is read through state.schema:
const isInline = editor.read(state => state.schema.isInline(element))Run a transaction. The callback receives tx, which owns command reads and
writes.
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 })
})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' },
},
}
)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)
})
})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' }] }],
},
},
})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'))Create, replace, or delete extra roots with tx.roots.
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.
state.value.get() returns the persisted document value.
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())State fields are registered with defineStateField and read through
state.getField(field).
const title = editor.read((state) => state.getField(documentTitle))Write state fields with tx.setField.
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'],
}
)See Document State for persistence patterns and comments ownership.
Schema setup belongs to extensions. Read schema policy through state.schema
or tx.schema.
import { defineEditorExtension, elementProperty } from '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.
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')
})
})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.
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)
}
})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)
}
})Call the returned function to unsubscribe.
Install an extension and return a cleanup function.
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 })
})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)Use editor.read(...) or editor.update(...) when a helper needs editor
state.