Skip to content

Selection Decoration

SelectionDecoration collapses range selections to a cursor when the editor loses focus. This prevents a “ghost selection” from lingering after the user clicks outside the editor, matching the behavior of Google Docs and Notion. Toolbar and bubble menu buttons use preventDefault() on mousedown, so they never trigger blur and the selection stays intact during editor UI interactions.

Included in StarterKit. Disable with StarterKit.configure({ selectionDecoration: false }).

import { Editor, Document, Paragraph, Text, SelectionDecoration } from '@domternal/core';
import '@domternal/theme';
const editor = new Editor({
element: document.getElementById('editor')!,
extensions: [Document, Paragraph, Text, SelectionDecoration],
});

Select text, then click outside the editor. The selection collapses to a cursor instead of remaining highlighted.

SelectionDecoration has no configurable options.

SelectionDecoration does not register any commands.

SelectionDecoration does not register any keyboard shortcuts.

SelectionDecoration does not register any input rules.

SelectionDecoration does not register any toolbar items.

SelectionDecoration creates a ProseMirror plugin with a handleDOMEvents.blur handler. When the editor’s contenteditable element loses focus:

  1. Checks if focus moved to an editor-related UI element (marked with [data-dm-editor-ui]). If so, does nothing.
  2. Checks if the current selection is a range (not collapsed). If from !== to, dispatches a transaction that collapses the selection to the from position.
blur(view, event) {
const related = event.relatedTarget;
if (related instanceof HTMLElement && related.closest('[data-dm-editor-ui]')) {
return false; // focus moved to editor UI, keep selection
}
const { from, to } = view.state.selection;
if (from !== to) {
view.dispatch(
view.state.tr.setSelection(TextSelection.create(view.state.doc, from))
);
}
return false;
}

Elements marked with the data-dm-editor-ui attribute are treated as part of the editor. When focus moves to these elements (e.g., a link popover input field), the selection is preserved. This prevents the selection from collapsing when the user interacts with:

  • Link popover URL inputs
  • Image popover fields
  • Any custom UI element with data-dm-editor-ui

Toolbar and bubble menu buttons call event.preventDefault() on mousedown. This prevents the browser from moving focus away from the editor, so the blur event never fires. The selection stays intact while clicking toolbar buttons, making SelectionDecoration transparent to toolbar interactions.

If the selection is already collapsed (cursor, from === to), the blur handler does nothing. Only range selections are affected.

import { SelectionDecoration, selectionDecorationPluginKey } from '@domternal/core';
import type { SelectionDecorationOptions } from '@domternal/core';
ExportTypeDescription
SelectionDecorationExtensionThe selection decoration extension
selectionDecorationPluginKeyPluginKeyThe ProseMirror plugin key
SelectionDecorationOptionsTypeScript typeOptions for SelectionDecoration.configure() (empty)

@domternal/core - SelectionDecoration.ts