Text selection is lost after programmatic DOM manipulation
OS: Windows 11 · Device: Desktop or Laptop Any · Browser: Chrome 120.0 · Keyboard: US
Open case →Scenario
After programmatically manipulating the DOM in a contenteditable element, restoring the text selection (cursor position) is unreliable across browsers. The selection may be lost, moved to an incorrect position, or become invalid.
After programmatically manipulating the DOM in a contenteditable element, restoring the text selection (cursor position) is unreliable across browsers. The selection may be lost, moved to an incorrect position, or become invalid.
Implement robust selection restoration:
function saveSelection() {
const selection = window.getSelection();
if (selection.rangeCount === 0) return null;
const range = selection.getRangeAt(0);
return {
startContainer: range.startContainer,
startOffset: range.startOffset,
endContainer: range.endContainer,
endOffset: range.endOffset,
commonAncestorContainer: range.commonAncestorContainer
};
}
function restoreSelection(saved) {
if (!saved) return false;
try {
const range = document.createRange();
range.setStart(saved.startContainer, saved.startOffset);
range.setEnd(saved.endContainer, saved.endOffset);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
return true;
} catch (e) {
// Selection is invalid, try to find nearest valid position
return restoreSelectionFallback(saved);
}
}
function restoreSelectionFallback(saved) {
// Find the common ancestor
let node = saved.commonAncestorContainer;
// Walk up to find a valid text node or element
while (node && node.nodeType !== Node.TEXT_NODE && node.nodeType !== Node.ELEMENT_NODE) {
node = node.parentNode;
}
if (!node) return false;
try {
const range = document.createRange();
if (node.nodeType === Node.TEXT_NODE) {
const length = node.textContent.length;
range.setStart(node, Math.min(saved.startOffset, length));
range.setEnd(node, Math.min(saved.endOffset, length));
} else {
range.selectNodeContents(node);
range.collapse(true);
}
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
return true;
} catch (e) {
return false;
}
}
// Use before DOM manipulation
element.addEventListener('beforeinput', (e) => {
if (e.inputType === 'insertText' || e.inputType === 'insertParagraph') {
const saved = saveSelection();
e.savedSelection = saved; // Store for later restoration
}
});
// Restore after manipulation
function manipulateDOM(callback) {
const saved = saveSelection();
callback();
// Restore selection after DOM updates
requestAnimationFrame(() => {
if (!restoreSelection(saved)) {
restoreSelectionFallback(saved);
}
});
}
Visual view of how this scenario connects to its concrete cases and environments. Nodes can be dragged and clicked.
Each row is a concrete case for this scenario, with a dedicated document and playground.
| Case | OS | Device | Browser | Keyboard | Status |
|---|---|---|---|---|---|
| ce-0110-selection-lost-after-dom-update | Windows 11 | Desktop or Laptop Any | Chrome 120.0 | US | draft |
| ce-0130-selection-invalid-after-wrap | Windows 11 | Desktop or Laptop Any | Firefox 120.0 | US | draft |
| ce-0138-selection-collapse-on-click | macOS 14.0 | Desktop or Laptop Any | Safari 17.0 | US | draft |
| ce-0149-selection-range-invalid-after-insert | Windows 11 | Desktop or Laptop Any | Firefox 120.0 | US | draft |
| ce-0172-selection-invalid-after-replace | Windows 11 | Desktop or Laptop Any | Safari 17.0 | US | draft |
This matrix shows which browser and OS combinations have documented cases for this scenario. Click on a cell to view the specific case.
| Browser | Windows | macOS |
|---|---|---|
| Chrome | — | |
| Firefox | — | |
| Safari |
Open a case to see the detailed description and its dedicated playground.
OS: Windows 11 · Device: Desktop or Laptop Any · Browser: Chrome 120.0 · Keyboard: US
Open case →OS: Windows 11 · Device: Desktop or Laptop Any · Browser: Firefox 120.0 · Keyboard: US
Open case →OS: macOS 14.0 · Device: Desktop or Laptop Any · Browser: Safari 17.0 · Keyboard: US
Open case →OS: Windows 11 · Device: Desktop or Laptop Any · Browser: Firefox 120.0 · Keyboard: US
Open case →OS: Windows 11 · Device: Desktop or Laptop Any · Browser: Safari 17.0 · Keyboard: US
Open case →Other scenarios that share similar tags or category.
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.
When setting cursor position using `selection.addRange()` in a contenteditable element, it works correctly in Chrome and Firefox but fails in Safari. The selection "pops out" of intended marker element and moves to the next sibling's text node instead of staying within the marker.
When selecting text that spans across multiple HTML elements (e.g., p, div, span) in a contenteditable region, the selection range may not accurately reflect the visual selection. The Selection and Range APIs may return incorrect boundaries.
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.
When editing content inside an element with `position:relative`, the text caret (cursor) is completely invisible. Text can be typed and appears in the editor, but there's no visual feedback of where the insertion point is located.
Have questions, suggestions, or want to share your experience? Join the discussion below.