Case ce-0571 · Scenario scenario-contenteditable-shadow-dom

Multiple selections collision in Shadow DOM

OS: macOS 14.6 Device: Desktop Any Browser: All Browsers Latest (2024) Keyboard: US QWERTY Status: confirmed
shadow-dom selection api-collision isolation

Phenomenon

The Web Selection API was designed for a single-selection-per-document model. When contenteditable is placed inside a Shadow Root, this model breaks. In many browsers (discussed heavily in 2024), the global window.getSelection() fails to deep-dive into the shadow tree, returning either the shadow host itself or a null range. Conversely, shadowRoot.getSelection() (where available) may report a range that the global document remains unaware of, leading to “Double Selection” UI or command execution failures.

Reproduction Steps

  1. Create a Custom Element with a Shadow Root.
  2. Inside the Shadow Root, append a div with contenteditable="true".
  3. Add some text outside the Custom Element.
  4. Select the external text.
  5. Click inside the shadow-based editor and start typing.
  6. Check window.getSelection() vs this.shadowRoot.getSelection().

Observed Behavior

  1. Selection Collision: The blue highlight from the external text may persist even though the caret is active inside the shadow root.
  2. API Inconsistency:
    • window.getSelection(): Often returns the #host element as the startContainer with an offset of 0, hiding the internal range.
    • document.activeElement: Correctly identifies the #host, but cannot reach the internal text node.
  3. Command Failure: Calling document.execCommand('bold') fails because the global selection doesn’t “see” the text node inside the shadow root.

Expected Behavior

The Selection API should provide a consistent path to the deepest active range, or browsers should automatically “forward” selection intent from the shadow root to the global document.

Impact

  • Framework Blindness: Modern editors (Lexical, Slate) that rely on window.getSelection() to find the caret position will throw errors or misplace nodes when used inside Web Components.
  • Accessibility: Screen readers may remain stuck on the selection outside the shadow root.
  • Broken Features: Built-in browser features like “Search in Page” or “Print Selection” often ignore content inside shadow-based contenteditable regions.

Browser Comparison

  • Chrome/Edge: Most advanced in supporting shadowRoot.getSelection(), but still exhibits global synchronization issues.
  • Safari: Historically inconsistent; window.getSelection() often becomes entirely unreliable when cross-boundary clicks occur.
  • Firefox: Most restrictive; has long-running bugs regarding selection crossing shadow boundaries.

References & Solutions

Mitigation: Selection Proxying

Capture selection changes inside the shadow root and manually sync them or use a proxy object for your editor.

this.shadowRoot.addEventListener('selectionchange', () => {
    const internalSel = this.shadowRoot.getSelection();
    // Manual sync logic for your framework
    if (internalSel.rangeCount > 0) {
        editor.updateSelection(internalSel.getRangeAt(0));
    }
});
Step 1: Shadow Root Setup
#shadow-root
Text in Shadow
An editor is encapsulated inside a Shadow Root via Web Components.
Step 2: External Selection
Document Text
#shadow-root ...
User selects text outside the shadow host.
vs
✅ Expected
Document Text
#shadow-root
[Text in Shadow]
Expected: window.getSelection() should correctly reflect the range within the shadow root, or the browser should provide a unified Selection proxy.

Playground for this case

Use the reported environment as a reference and record what happens in your environment while interacting with the editable area.

Reported environment
OS: macOS 14.6
Device: Desktop Any
Browser: All Browsers Latest (2024)
Keyboard: US QWERTY
Your environment
Sample HTML:
Event log
Use this log together with the case description when filing or updating an issue.
0 events
Interact with the editable area to see events here.

Comments & Discussion

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