Plate stores selection in the editor model and projects text selection to the browser through the mounted editor. This guide covers model reads and updates, focus, rendering, and the browser boundary.
The editor model is the single selection state. Each rendering layer derives its presentation from that state.
| Job | Owner |
|---|---|
| Selection state, mapping, validation, reads, and updates | platejs |
| Native DOM selection, focus synchronization, and inactive paint | EditorContent |
| Exact node-selection highlighting and pointer-drag input | and from |
NodeSelectionHighlightNodeSelectionDragplatejs/react| Focus markers, colors, borders, and stacking | Copied registry UI |
editor.read.selection() returns the active representative Range or null.
For an exact node selection, use selection.nodes() or selection.ranges()
instead of treating that representative range as exact membership.
const range = editor.read.selection();
const expanded = editor.read.selection.isExpanded();
const selectedNodes = editor.read.selection.nodes();
const exactRanges = editor.read.selection.ranges();
editor.update.selection.set({
anchor: { path: [0, 0], offset: 0 },
focus: { path: [0, 0], offset: 5 },
});
editor.update.selection.setNodes([[0], [2]]);
editor.update.selection.collapse({ edge: 'end' });
editor.update.selection.set(null);const range = editor.read.selection();
const expanded = editor.read.selection.isExpanded();
const selectedNodes = editor.read.selection.nodes();
const exactRanges = editor.read.selection.ranges();
editor.update.selection.set({
anchor: { path: [0, 0], offset: 0 },
focus: { path: [0, 0], offset: 5 },
});
editor.update.selection.setNodes([[0], [2]]);
editor.update.selection.collapse({ edge: 'end' });
editor.update.selection.set| Model state | selection() | Exact membership | Browser presentation |
|---|---|---|---|
| Text selection | One Range | ranges() returns that range | Native browser selection while the mounted editor is focused |
| Node selection | One representative Range | nodes() and ranges() return every selected node | Plate node-selection primitives; the native selection stays empty |
| No selection | null | Empty arrays | None |
Use Editor Methods for editor access inside React and Selection API for the complete query and validation contracts.
Updating the model selection does not require direct DOM selection writes. When an action should return input to the editor, set the selection and focus the mounted editor explicitly:
editor.update.selection.set(range);
editor.api.dom.focus();editor.update.selection.set(range);
editor.api.dom.focus();Mouse, keyboard, touch, composition, and native input can move the browser
selection. The mounted editor imports those changes into the model and exports
model updates back to the DOM. Application code should not patch
window.getSelection() for normal editor behavior.
Plate EditorContent renders an inactive selection when focus moves to a marked
control. The behavior is built in; it does not require a prop, plugin, kit, or
copied selection state.
Add data-editor-keep-selection-visible to a focusable control or any composed
ancestor:
<div data-editor-keep-selection-visible="">
<button type="button">Edit link</button>
</div><div data-editor-keep-selection-visible="">
<button type="button">Edit link</button>
</div>The exact EditorContent that loses focus reads its live canonical selection. An
expanded range renders a highlight. A collapsed range renders a caret. Focus
returning to the editor or moving to an unmarked target removes the inactive
paint.
Plate publishes neutral output hooks. Product CSS owns their appearance:
[data-editor-inactive-selection] {
background: color-mix(in oklab, var(--primary) 25%, transparent);
}
[data-editor-inactive-selection-caret] {
width: 2px;
background: var(--primary);
}[data-editor-inactive-selection] {
background: color-mix(in oklab, var(--primary) 25%, transparent);
}
[data-editor-inactive-selection-caret] {
width: 2px;
background: var(--primary);
}The copied Editor registry component already styles both hooks.
| Focus target | Inactive paint |
|---|---|
| Marked control or composed ancestor | One highlight or caret from the originating EditorContent |
| The editor | None; the browser owns the active selection |
| Any unmarked control | None |
| Window blur or missing focus target | None |
The marker controls presentation only. It does not modify the document, selection, history, clipboard, or collaboration state.
Exact node selection is model state, not a native browser range. The copied
Editor composes NodeSelectionHighlight and NodeSelectionDrag after
EditorContent to render selected blocks and support blank-space pointer drag.
Use the Node Selection components for the DOM contract and the Node Selection demo for the copied Editor composition. Feature UI such as Block Menu reads the same exact node membership instead of maintaining another selected-node store.
Keep feature-specific behavior with the feature that owns it:
| Need | Guide |
|---|---|
| Configure mark and inline boundary affinity | Editing Behavior and Plugin Rules |
| Apply context-menu actions to selected blocks | Block Menu |
| Search text with transient match highlights | Find |
| Preserve selection while editing a link | Link |
| Publish and render remote selections | Collaboration |
| Build a tag-oriented select editor | Multi Select |
Each mounted EditorContent orders post-model work: model updates, DOM reads,
DOM or React writes, then selection export and repair. Focus restoration,
scrolling, composition repair, and selection projection share that view's
lifetime. Normal application actions update model selection and let this owner
reconcile the browser.
Horizontal caret movement follows the browser's visual order, including mixed
direction text. Pure point transforms remain logical and work without DOM.
Custom integrations can inspect a mounted caret step with
editor.api.dom.resolveVisualPoint(point, { direction: 'left', unit: 'word' }).
A missing DOM mapping returns null.
DOM coverage declares whether hidden content is skipped, selectable in the model, or materialized before selection enters it. Copy has a separate policy and can include model content that has no mounted DOM.
An external text view keeps canonical text in the document. Only the focused adapter owns native selection. Other views paint model selection without moving focus. A selection crossing native and external text remains complete in the model; its partial native range must not overwrite it. The adapter owns its own search, accessibility, and print integration.
Virtualized rendering omits offscreen blocks. A model selection can span the complete document while browser selection and find only see mounted content. Use a complete view when native search, accessibility traversal, printing, or DOM integrations require the whole tree.
Use the Browser helpers to inspect the model selection, native caret or range, focus, and displayed highlights. After asserting the target state, type again and verify the text and caret. A correct model range alone does not establish that browser input and visible selection are correct.
A non-void, isolating element can declare schema.element.keyboardSelectable: true
when it owns both an asset and editable child content. Selecting the owner node
focuses the asset and keeps native selection empty. ArrowDown enters its text;
ArrowUp from the leading visual boundary returns to node selection. Backspace
or Delete removes a selected owner.
Clicking a noneditable descendant selects the owner; clicking editable text
selects text. Use useElementSelected({ mode: 'node' }) for the exact asset
selection. The default intersection mode also matches text selection inside it.