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

Selection And DOM

PreviousNext

Understand model selection, browser-native selection, DOM coverage, voids, hidden content, and browser proof.

Plite stores selection in the model, then reconciles it with browser-native selection in React. Use this page for caret and DOM rules; use Editing Behavior for the full event-to-commit pipeline.

Choose the right assertion

Selection bugs need the layer that can actually fail.

ClaimAssertOwner
The Plite selection is correctmodel selectionplitejs
The browser caret lands in the right DOM textDOM caret or DOM selection and
Editing BehaviorClipboard And Paste

On This Page

Choose the right assertionSelection ownershipModel selectionSelection queriesNative selectionObject elements with editable childrenDOM phase orderDOM coverageExternal text viewsVoids and editable contentLarge documentsBrowser proofReference
Build your editor
Production-ready AI template and reusable components.
Get all-access
plitejs/react
plitejs/dom
The visible browser highlight is not duplicateddisplayed selection snapshot@platejs/test
Hidden content remains selectable or copyableDOM coverage policy plus model resultplitejs/dom and plitejs/react
Follow-up typing still workstext, selection, focus, and commit after the asserted state@platejs/test

Do not close a selection bug with only one layer unless the bug only exists in that layer.

Selection ownership

Plite has two selection systems in a React editor:

SelectionWhat it means
Model selectionThe serializable TextSelection, NodeSelection, or null stored by the Plite editor.
Native selectionThe browser Selection visible in the contenteditable DOM. Only text selection projects to it.

Most editor code should write the model selection and let Plite export the DOM/native selection. Plite imports the native selection when the browser is the current source of truth, such as user selection movement or native input.

Model selection

Read selection through editor.read(...).

const selection = editor.read((state) => state.selection());
const selection = editor.read((state) => state.selection());

Write selection inside editor.update(...).

import { SelectionApi } from "plitejs";
 
editor.update((tx) => {
  tx.selection.set(
    SelectionApi.text({
      anchor: { path: [0, 0], offset: 0 },
      focus: { path: [1, 0], offset: 3 },
    })
  );
});
import { SelectionApi } from "plitejs";
 
editor.update((tx) => {
  tx.selection.set(
    SelectionApi.text({
      anchor: { path: [0, 0], offset: 0 },
      focus: { path: [1, 0], offset: 3 },
    })
  );
});

Selection queries

Use explicit predicates instead of rebuilding point and block comparisons in feature code.

const acrossBlocks = editor.read.selection.isAcrossBlocks();
const atHeadingStart = editor.read.selection.isAtBlockStart({
  type: "heading",
});
const intersectsIntro = editor.read.selection.intersects([0]);
const containsRange = editor.read.selection.contains(range);
const atWordEnd = editor.read.points.isWordEnd(point);
const acrossBlocks = editor.read.selection.isAcrossBlocks();
const atHeadingStart = editor.read.selection.isAtBlockStart({
  type: "heading",
});
const intersectsIntro = editor.read.selection.intersects([0]);
const containsRange = editor.read.selection.contains(range);
const atWordEnd = editor.read.points.isWordEnd(point);

isWithinBlock, isAcrossBlocks, isAtBlockStart, and isAtBlockEnd accept at, type, and match options. Omit at to inspect the current selection. These queries return false when the target or current selection does not resolve.

Use Locations for Path, Point, Range, and anchors. Use Transforms for transaction selection helpers.

Native selection

The browser can move native selection through mouse, keyboard, touch, composition, spellcheck, selection handles, and platform editing commands. Plite React observes those moves and imports them when they should become model state.

Application code should not patch the browser Selection directly for normal editor behavior. Use model transactions, Editable handlers, or DOM-aware React APIs that preserve Plite's repair path.

Use React Editor for DOM translation helpers and Plite React Event Handling for browser event hooks.

Horizontal arrow and word movement are visual DOM operations. Plite React asks the DOM capability for the next physical left or right point and affinity, so mixed-direction text follows the browser's rendered order rather than one block-wide direction guess. Core point transforms remain logical and DOM-free.

const next = editor.api.dom.resolveVisualPoint(point, {
  affinity: "forward",
  direction: "left",
  unit: "word",
});
const next = editor.api.dom.resolveVisualPoint(point, {
  affinity: "forward",
  direction: "left",
  unit: "word",
});

This low-level API is useful for renderer integrations. Normal editors should let <Editable> own keyboard movement.

Object elements with editable children

Use a block element with object: true when one document node owns both non-editable asset chrome and editable child content. The object remains non-void, selectable, and structurally isolating. The model keeps its focus states distinct:

StateBehavior
NodeSelection at the owner pathFocuses the asset. The native browser selection stays empty. ArrowDown enters the owner's text content, and Backspace or Delete removes the owner.
TextSelection inside the ownerEdits child content. ArrowUp from its leading visual boundary returns to the owner NodeSelection.

Clicking a contenteditable="false" descendant of an object owner creates its NodeSelection. Clicking editable child text creates a normal TextSelection.

import { definePlugin, schema } from "plitejs";
 
const media = definePlugin("media", {
  schema: {
    elements: {
      image: {
        content: schema.content.text({ default: "text", min: 1 }),
        object: true,
      },
    },
  },
});
import { definePlugin, schema } from "plitejs";
 
const media = definePlugin("media", {
  schema: {
    elements: {
      image: {
        content: schema.content.text({ default: "text", min: 1 }),
        object: true,
      },
    },
  },
});

In React node UI, use useElementSelected({ mode: "node" }) for the exact asset-focused state. The default intersection mode also matches text selection inside the element.

An object with an empty child text is still meaningful content. An owner NodeSelection copies the complete object, while a TextSelection inside its children copies open child content without recreating the owner on paste.

DOM phase order

Each mounted Editable owns one bounded scheduler for post-model browser work. It runs queued tasks in this order:

  1. model work
  2. DOM reads
  3. DOM or React writes
  4. selection export and repair
  5. post-selection work

Selection export, scroll restoration, native-input repair, drag auto-scroll, projected caret cleanup, focus restoration, and DOM repair share that owner. Semantic browser clocks such as composition guard lifetimes can wait on a timer, but their DOM or selection side effects re-enter the phase scheduler. Recursive scheduling is bounded and recorded as a loop diagnostic instead of spinning indefinitely.

DOM coverage

DOM coverage boundaries represent model content whose editable DOM is not mounted.

PolicyUse it when
selectionPolicy="skip"Hidden content is app chrome or should not receive cursor movement.
selectionPolicy="model"Hidden content can be selected in the Plite model without mounting DOM.
selectionPolicy="materialize"Plite should ask the app to reveal content before moving selection into it.

Clipboard has its own policy. Model-backed copy can include hidden document content without selecting every hidden DOM node. Use Clipboard And Paste for copy, paste, drop, and fragment import ownership.

Use DOM Coverage Boundaries for selectionPolicy, copyPolicy, and materialization behavior.

Coverage belongs to one mounted view, not the editor globally. Native and external views of the same document can therefore have different coverage.

External text views

An external text view replaces native Plite rendering for one Text without replacing its canonical model. Only the focused adapter may own native selection. Other views paint model selection without moving browser selection or focus.

A cross-boundary selection stays complete in Plite even when only its visible native part can form a DOM range. The adapter paints its intersecting range, and Plite copy/cut uses the complete canonical selection. A partial native range must never overwrite that model range during reconciliation.

The adapter owns local search, accessibility, and print behavior. Keeping text in the model does not make unmounted text searchable by native browser find.

Voids and editable content

Void elements are atomic in the outer document. Plite renders their hidden text anchor so selection can move around them. Native form controls belong inside the noneditable void UI; they do not edit that anchor.

Declare an atomic block with void: "block":

import { definePlugin } from "plitejs";
 
const media = definePlugin("media", {
  schema: {
    elements: {
      image: {
        void: "block",
      },
    },
  },
});
import { definePlugin } from "plitejs";
 
const media = definePlugin("media", {
  schema: {
    elements: {
      image: {
        void: "block",
      },
    },
  },
});

Install this feature contribution beside the editor's complete schema.

Keep visible void UI inside the void renderer. Let Plite render the shell and text anchor. Use Editable Component for void rendering rules.

Keep editable document text in an ordinary element. Use a named content root when a void's rich body needs independently addressed content with the same editor runtime and history.

Large documents

Editable mounts the complete document DOM. VirtualizedEditable from plitejs/react/virtualized explicitly omits offscreen top-level blocks when a large document exceeds the product's tested DOM budget.

In a virtualized view, a model selection can cover the full document while the mounted DOM covers the measured window, selected endpoints, and requested targets. Native browser selection and find can prove facts only about mounted content.

Use Editable Component for the complete surface and Virtualized Rendering for explicit omission.

Browser proof

Selection proof should assert the layers named by the claim. The app under test installs the browser handle first, as Editing Behavior shows.

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


Use Playwright helpers to capture model selection, DOM selection, displayed selection, focus, native event traces, clipboard, screenshots, and follow-up typing.

Reference

  • Range API
  • Point API
  • Transforms API
  • Clipboard And Paste
  • Decorations, annotations, and widgets
  • React Editor
  • DOM Coverage Boundaries
  • Browser proof
"Hello"
, offset:
5
});
await editor.assert.noDoubleSelectionHighlight();
await editor.type("!");
await editor.assert.text("Hello!");