Editing Behavior

PreviousNext

Understand how browser input, transactions, operations, normalization, commits, rendering, and proof fit together.

Editing behavior is the path from user intent to committed Slate state. Use this page for the runtime pipeline; use Selection And DOM for caret, native selection, and DOM coverage rules.

Choose The Right Surface

Most editing bugs come from putting behavior in the wrong layer.

SurfaceUse it whenOwner
Editable event propsOne React editable needs a local browser shortcut or event hook.@platejs/slate-react
editor.update((tx) => ...)A command should change the document, selection, marks, roots, or state.@platejs/slate
Extension transformsA reusable behavior should affect keyboard, native input, programmatic transforms, and tests.@platejs/slate
Extension clipboard.insertDataPaste or drop ingress needs package-owned policy.@platejs/slate plus host clipboard data
DOM coverage boundariesModel content exists but its DOM is intentionally hidden or virtualized.@platejs/slate-dom and @platejs/slate-react
@platejs/browserA behavior claim needs model, DOM, native selection, focus, trace, screenshot, or follow-up typing proof.@platejs/browser

Use Editable for UI-local event interception. Use transactions and extensions for editor behavior that should survive another input path.

Runtime Pipeline

Slate edits run through explicit owners.

StageWhat happensOwner
Browser eventThe browser sends key, beforeinput, input, paste, cut, drop, focus, drag, or selection events.Browser
Editable handlerEditable runs app handlers and decides whether Slate should continue.@platejs/slate-react
Input importSlate imports the relevant DOM/native selection when the browser owns the current edit target.@platejs/slate-react and @platejs/slate-dom
Transform middlewareExtension transform hooks can handle, wrap, or pass through semantic edit names.@platejs/slate
Transactioneditor.update((tx) => ...) groups model writes into one runtime change.@platejs/slate
OperationsSlate records low-level operations for document, selection, root, and state changes.@platejs/slate
NormalizationBuilt-in and extension normalizers repair invalid document shapes.@platejs/slate
CommitSubscribers, history, React, replay, collaboration adapters, and proof tools observe one committed change.@platejs/slate
Render and repairReact renders the new state and exports a valid DOM/native selection when needed.@platejs/slate-react
ProofBrowser tests assert the model, DOM, native selection, focus, trace, and follow-up typing that matter for the claim.@platejs/browser

The important rule is simple: user intent can arrive through many browser paths, but Slate behavior should land in the transaction pipeline when it changes editor state.

Event Handlers

Editable event props are the right tool for editor-local UI behavior.

import { Editable } from "@platejs/slate-react";
 
<Editable
  onKeyDown={(event, { editor }) => {
    if (!(event.metaKey && event.key === "k")) return false;
 
    editor.update((tx) => {
      tx.text.insert("link");
    });
 
    return true;
  }}
/>;
import { Editable } from "@platejs/slate-react";
 
<Editable
  onKeyDown={(event, { editor }) => {
    if (!(event.metaKey && event.key === "k")) return false;
 
    editor.update((tx) => {
      tx.text.insert("link");
    });
 
    return true;
  }}
/>;

Return true when your handler owns the event. Return false when Slate should keep running its default behavior.

Use Slate React Event Handling for the exact handler return contract.

Transactions

Transactions are the write boundary. They group related changes and publish one commit.

editor.update((tx) => {
  tx.text.insert("Hello");
  tx.marks.toggle("bold");
  tx.selection.collapse({ edge: "end" });
});
editor.update((tx) => {
  tx.text.insert("Hello");
  tx.marks.toggle("bold");
  tx.selection.collapse({ edge: "end" });
});

Keep writes inside one update when they belong to one user action. That gives history, subscribers, React rendering, operation replay, and proof tooling one change to observe.

Use Transforms for the concept guide and Transforms API for the exact transaction groups.

Transform Middleware

Reusable behavior belongs in extension transform middleware when it should apply outside one React event.

import { defineEditorExtension, RangeApi } from "@platejs/slate";
 
const shortcuts = defineEditorExtension({
  name: "shortcuts",
  transforms: {
    insertText({ next, text, tx }) {
      const selection = tx.selection.get();
 
      if (text === " " && selection && RangeApi.isCollapsed(selection)) {
        tx.nodes.set({ type: "heading-one" });
        return true;
      }
 
      return next();
    },
  },
});
import { defineEditorExtension, RangeApi } from "@platejs/slate";
 
const shortcuts = defineEditorExtension({
  name: "shortcuts",
  transforms: {
    insertText({ next, text, tx }) {
      const selection = tx.selection.get();
 
      if (text === " " && selection && RangeApi.isCollapsed(selection)) {
        tx.nodes.set({ type: "heading-one" });
        return true;
      }
 
      return next();
    },
  },
});

Use this for semantic edit names such as insertText, insertBreak, deleteBackward, deleteForward, and paste insertion policy. It keeps behavior available to keyboard input, native input, programmatic commands, tests, and browser proof.

Operations And Normalization

Operations are the replay boundary. Slate records the low-level changes produced by a transaction, then normalizes the affected tree.

editor.update((tx) => {
  tx.operations.replay(remoteOperations, {
    tag: "remote-import",
  });
});
editor.update((tx) => {
  tx.operations.replay(remoteOperations, {
    tag: "remote-import",
  });
});

Use Operations when you need the replay model. Use Normalizing when a structural edit can leave the document temporarily invalid.

Commits And React

A finished update publishes one commit. Runtime subscribers can observe it, history can batch it, React can render from it, and browser proof can inspect the aftermath.

const unsubscribe = editor.subscribe((_snapshot, change) => {
  if (change?.childrenChanged || change?.dirtyStateKeys.length) {
    saveDocument(editor.read((state) => state.value.get()));
  }
});
const unsubscribe = editor.subscribe((_snapshot, change) => {
  if (change?.childrenChanged || change?.dirtyStateKeys.length) {
    saveDocument(editor.read((state) => state.value.get()));
  }
});

React UI should read narrow editor facts through hooks where possible. App services can subscribe to commits when they need persistence, analytics, replay, or sync work.

Browser Proof

Model-only tests do not prove browser editing. DOM-only tests do not prove Slate correctness.

import { openExample } from "@platejs/browser/playwright";
 
const editor = await openExample(page, "plaintext", {
  ready: { editor: "visible" },
});
 
await editor.focus();
await editor.type("Hello");
await editor.assert.text("Hello");
await editor.assert.selection({
  anchor: { path: [0, 0], offset: 5 },
  focus: { path: [0, 0], offset: 5 },
});
await editor.assert.noDoubleSelectionHighlight();
import { openExample } from "@platejs/browser/playwright";
 
const editor = await openExample(page, "plaintext", {
  ready: { editor: "visible" },
});
 
await editor.focus();
await editor.type("Hello");
await editor.assert.text("Hello");
await editor.assert.selection({
  anchor: { path: [0, 0], offset: 5 },
  focus: { path: [0, 0], offset: 5 },
});
await editor.assert.noDoubleSelectionHighlight();

Use Slate Browser when a claim depends on browser events, focus, native selection, screenshots, clipboard, replay, or follow-up typing.

Recipes

GoalStart with
Add one local hotkeySlate React Event Handling
Write a reusable commandCommands and Transforms
Change Enter, Backspace, Delete, or typed text behaviorExtensions
Preserve valid document shapeNormalizing
Replay operations from storage or syncOperation Replay Substrate
Debug caret or DOM selection bugsSelection And DOM
Own paste, copy, drop, or fragment import policyClipboard And Paste
Build comments, highlights, diagnostics, or overlay UIProjection And Overlays
Prove a browser editing claimSlate Browser

Done. You can now place an editing behavior in the layer that owns it.