Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Overview
    • Why This Fork
    • Examples
    • Installing Plite
    • Adding Event Handlers
    • Defining Custom Elements
    • Applying Custom Formatting
    • Executing Commands
    • Saving to a Database
    • Canonical Change Substrate
    • Improving Performance
    • Interfaces
    • Nodes
    • Locations
    • Transforms
    • Document Changes
    • Commands
    • Editor
    • Plugins
    • Rendering
    • Serializing
    • Normalizing
    • Using TypeScript
    • Roots
    • Document State
    • Editing Behavior
    • Selection And DOM
    • Clipboard And Paste
    • Decorations, annotations, and widgets
    • Schema
    • Anchor API
    • Location API
    • Path API
    • PointEntry API
    • Point API
    • Range API
    • Selection API
    • Location Types APIs
    • Span API
    • Editor
    • Element API
    • NodeEntry API
    • Node API
    • Node Types APIs
    • Text API
    • Debug Value Scrubbing
    • Transforms API
    • Plite DOM
    • History Editor API
    • History Plugin Setup
    • History
    • Plite History
    • Plite Hyperscript
    • Plite PaginationExperimental
    • Annotations
    • DOM Coverage Boundaries
    • Editable Component
    • External Text Views
    • Plite React Event Handling
    • Virtualized Rendering
    • Plite React Hooks
    • React Editor Setup
    • React Editor
    • Plite React
    • Plite Component
    • Plite Yjs
    • Plite
    • Migration
    • Contributing
    • FAQ
    • Resources

Editing Behavior

PreviousNext

Understand how browser input, transactions, changes, corrections, commits, rendering, and proof fit together.

Editing behavior is the path from user intent to committed Plite 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.plitejs/react
editor.update((tx) => ...)
Document StateSelection And DOM

On This Page

Choose the right surfaceRuntime pipelineEvent handlersTransactionsCommand handlersChanges and correctionsCommits and ReactBrowser proofRecipes
Build your editor
Production-ready AI template and reusable components.
Get all-access
A command should change the document, selection, marks, roots, or state.
plitejs
Plugin commandsA reusable semantic action should compose across browser, programmatic, and test callers.plitejs
domCommands.insertData interceptorPaste or drop ingress needs package-owned DOM policy.plitejs/dom
DOM coverage boundariesModel content exists but its DOM is intentionally hidden or virtualized.plitejs/dom and plitejs/react
@platejs/testA behavior claim needs model, DOM, native selection, focus, trace, screenshot, or follow-up typing proof.@platejs/test

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

Runtime pipeline

Plite 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 Plite should continue.plitejs/react
Input importPlite imports the relevant DOM/native selection when the browser owns the current edit target.plitejs/react and plitejs/dom
Command dispatchPure plugin command handlers consume, delegate, or compose typed semantic actions.plitejs
Transactioneditor.update((tx) => ...) groups model writes into one runtime change.plitejs
Canonical changePlite builds one root-aware DocumentChange for the complete transaction.plitejs
CorrectionsPlugin corrections repair changed ranges; construction and fitting enforce canonical shape.plitejs
CommitSubscribers, history, React, replay, collaboration adapters, and proof tools observe one committed change.plitejs
Render and repairReact renders the new state and exports a valid DOM/native selection when needed.plitejs/react
ProofBrowser tests assert the model, DOM, native selection, focus, trace, and follow-up typing that matter for the claim.@platejs/test

The important rule is simple: user intent can arrive through many browser paths, but Plite 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 "plitejs/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 "plitejs/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 Plite should keep running its default behavior.

Use Plite 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, change replay, and proof tooling one commit to observe.

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

Command handlers

Reusable semantic behavior belongs in pure plugin command handlers when it should apply outside one React event.

import {
  definePlugin,
  editorCommands,
  RangeApi,
} from "plitejs";
 
const shortcuts = definePlugin("shortcuts", {
  commands: ({ handle }) => [
    handle(editorCommands.insertText, ({ input, state }) => {
      const selection = state.selection();
 
      if (input.text !== " " || !selection || !RangeApi.isCollapsed(selection)) {
        return false;
      }
 
      return state.transaction((tx) => {
        tx.nodes.set({ type: "heading-one" });
        tx.text.insert(input.text, input.options);
      });
    }),
  ],
});
import {
  definePlugin,
  editorCommands,
  RangeApi,
} from "plitejs";
 
const shortcuts = definePlugin("shortcuts", {
  commands: ({ handle }) => [
    handle(editorCommands.insertText, ({ input, state }) => {
      const selection = state.selection();
 
      if (input.text !== " " || !selection || !RangeApi.isCollapsed(selection)) {
        return false;
      }
 
      return state.





Use built-in definitions such as editorCommands.insertText, editorCommands.insertBreak, editorCommands.delete, and editorCommands.replaceSlice. A handler returns false or an immutable TransactionSpec. Returning false delegates to the next handler or the built-in implementation. Use around(descriptor, handler) from the command factory only when a handler must rewrite input or compose with downstream behavior.

Changes and corrections

DocumentChange is the replay boundary. Plite corrects the affected regions before publishing the final change.

editor.update({ tags: "remote-import" }, (tx) => {
  tx.changes.apply(DocumentChange.fromJSON(remoteChange));
});
editor.update({ tags: "remote-import" }, (tx) => {
  tx.changes.apply(DocumentChange.fromJSON(remoteChange));
});

Use Document Changes for replay and mapping. 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.subscribeCommit((commit) => {
  if (commit.changed.has("document") || commit.dirtyStateKeys.length > 0) {
    saveDocument(editor.read.value());
  }
});
const unsubscribe = editor.subscribeCommit((commit) => {
  if (commit.changed.has("document") || commit.dirtyStateKeys.length > 0) {
    saveDocument(editor.read.value());
  }
});

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 Plite correctness.

@platejs/test reads the editor through a browser handle that the app under test installs. Call installBrowserHandle() once at module scope in a test or development entry, before the first editor mounts. Production entries never call it, so their bundles drop the handle.

import { installBrowserHandle } from "plitejs/react";
 
installBrowserHandle();
import { installBrowserHandle } from "plitejs/react";
 
installBrowserHandle();

Plate apps import the same installer from platejs/react.

Each Playwright project sets use.clipboardTransport to 'native', 'event' or 'handle', typed by BrowserTestOptions from @platejs/test/playwright. Clipboard paste helpers fail when that transport did not apply the paste.

import { openExample } from "@platejs/test/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/test/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 browser proof when a claim depends on browser events, focus, native selection, screenshots, clipboard, replay, or follow-up typing.

Recipes

GoalStart with
Add one local hotkeyPlite React Event Handling
Write a reusable commandCommands and Transforms
Change Enter, Backspace, Delete, or typed text behaviorCommands and Plugins
Preserve valid document shapeNormalizing
Apply document changes from storage or syncCanonical Change Substrate
Debug caret or DOM selection bugsSelection And DOM
Own paste, copy, drop, or fragment import policyClipboard And Paste
Build comments, highlights, diagnostics, or floating UIDecorations, annotations, and widgets
Prove a browser editing claimBrowser proof

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

transaction
((
tx
)
=>
{
tx.nodes.set({ type: "heading-one" });
tx.text.insert(input.text, input.options);
});
}),
],
});