Scenario

Contenteditable isolation and selection in Shadow DOM

Analysis of the architectural friction between the single-selection document model and Web Component encapsulation.

other
Scenario ID
scenario-contenteditable-shadow-dom

Details

Problem Overview

The Web Selection API was standardized long before the widespread adoption of Shadow DOM. This architectural gap creates a “Selection Blindness” phenomenon: the global window.getSelection() model often treats a Shadow Host as an opaque block, while the internal shadowRoot.getSelection() provides a range that the rest of the application (and even some browser systems like find-in-page) cannot see.

Observed Behavior

Scenario 1: Selection Collision (Double-Highlight)

When a user moves focus from a global text selection to a contenteditable region inside a Shadow Root, many browsers fail to correctly clear the previous highlight, leading to confusing “double-selection” UI.

/* Conflict Logic */
// 1. User selects "Hello World" in the light DOM.
// 2. User clicks inside <my-editor> (Shadow DOM).
// 3. window.getSelection() still reports "Hello World".
// 4. shadowRoot.getSelection() reports the active caret in the editor.

Scenario 2: Command Execution Failure

Calling document.execCommand() typically targets the range reported by window.getSelection(). If this selection is stuck at the shadow-host boundary, global commands (like ‘bold’ or ‘italic’) will do nothing to the text inside the editor.

Impact

  • Framework Blindness: Editor engines (Slate, Lexical) that rely on selectionchange on the document often miss events happening inside Shadow Roots.
  • Accessibility Isolation: Accessibility trees sometimes fail to bridge the gap zwischen the shadow-based caret and the document-level focus indicator.

Browser Comparison

  • Blink (Chrome/Edge): Supports shadowRoot.getSelection(), but synchronization with the light DOM selection is often laggy or inconsistent.
  • Gecko (Firefox): Historically had the most restricted support for selection crossing shadow boundaries; requires heavy manual polyfilling.
  • WebKit (Safari): Notable for “unbreakable” light-DOM selections that stay active even when focus is deep within a Shadow Root.

Solutions

1. Manual Selection Syncing

Listen to selectionchange within the shadow root and explicitly notify your state manager.

this.shadowRoot.addEventListener('selectionchange', () => {
  const selection = this.shadowRoot.getSelection();
  if (selection.rangeCount > 0) {
    this.dispatchEvent(new CustomEvent('editor-selection-change', {
      detail: selection.getRangeAt(0),
      bubbles: true,
      composed: true
    }));
  }
});

References

Scenario flow

Visual view of how this scenario connects to its concrete cases and environments. Nodes can be dragged and clicked.

React Flow mini map

Variants

Each row is a concrete case for this scenario, with a dedicated document and playground.

Case OS Device Browser Keyboard Status
ce-0571 macOS 14.6 Desktop Any All Browsers Latest (2024) US QWERTY confirmed

Cases

Open a case to see the detailed description and its dedicated playground.

Related Scenarios

Other scenarios that share similar tags or category.

Tags: selection

Selection mismatch between beforeinput and input events

The selection (window.getSelection()) in beforeinput events can differ from the selection in corresponding input events. This mismatch can occur during IME composition, text prediction, or when typing adjacent to formatted elements like links. The selection in beforeinput may include adjacent formatted text, while input selection reflects the final cursor position.

1 case
Tags: selection

Browser zoom causes caret and selection positioning issues

When the browser is zoomed (or content is scaled via CSS transforms), caret position and text selection in contenteditable elements can become inaccurate. Clicking at a certain position places the caret elsewhere, and selection highlights may not match the visual selection.

1 case
Tags: selection

Cursor disappears with contenteditable="false" elements

When contenteditable='false' elements are placed inside a contenteditable container, the cursor may disappear or become invisible in certain browsers, making it difficult for users to determine the text insertion point.

0 cases

Comments & Discussion

Have questions, suggestions, or want to share your experience? Join the discussion below.