Phenomenon
In Chrome, when the DOM is manipulated during user input in a contenteditable element, the caret may unexpectedly jump to the end of the content. This often occurs when programmatically inserting or deleting non-editable elements like <span contenteditable="false"> while the user is typing.
Reproduction example
- Create a contenteditable div with some text.
- Programmatically insert or delete a non-editable
<span contenteditable="false">element. - Observe that the caret jumps to the end of the contenteditable element.
- This disrupts the userโs typing position.
Observed behavior
- Caret jumps to end: Caret position resets to end of contenteditable element.
- DOM manipulation trigger: Occurs when DOM is modified during user input.
- Non-editable elements: More common when working with
contenteditable="false"elements. - Chrome-specific: This behavior is more prevalent in Chrome.
- User disruption: Severely disrupts typing and editing experience.
Expected behavior
- Caret position should be preserved during DOM manipulations.
- Programmatic changes should not affect userโs cursor position.
- Editing should continue smoothly even when DOM is modified.
Analysis
Chromeโs contenteditable implementation may reset the selection when DOM structure changes, especially when non-editable elements are involved. The browserโs selection management doesnโt properly track position across structural changes.
Workarounds
- Preserve caret position before DOM manipulation:
function saveCaretPosition(element) { const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); return { startContainer: range.startContainer, startOffset: range.startOffset, endContainer: range.endContainer, endOffset: range.endOffset }; } return null; } function restoreCaretPosition(element, savedPosition) { if (!savedPosition) return; const range = document.createRange(); range.setStart(savedPosition.startContainer, savedPosition.startOffset); range.setEnd(savedPosition.endContainer, savedPosition.endOffset); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); } - Insert non-breaking space after non-editable elements to maintain position.
- Use block-level elements instead of inline elements for contenteditable regions.
- Avoid DOM manipulation during active input, defer to next frame.