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
- Create a Custom Element with a Shadow Root.
- Inside the Shadow Root, append a
divwithcontenteditable="true". - Add some text outside the Custom Element.
- Select the external text.
- Click inside the shadow-based editor and start typing.
- Check
window.getSelection()vsthis.shadowRoot.getSelection().
Observed Behavior
- Selection Collision: The blue highlight from the external text may persist even though the caret is active inside the shadow root.
- API Inconsistency:
window.getSelection(): Often returns the#hostelement as thestartContainerwith an offset of 0, hiding the internal range.document.activeElement: Correctly identifies the#host, but cannot reach the internal text node.
- 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
contenteditableregions.
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));
}
});