Phenomenon
When using contenteditable elements in Vue with reactive state binding, the caret (cursor) jumps to the beginning of the element whenever the component re-renders due to state changes. This behavior is particularly noticeable in Safari and Firefox.
Reproduction example
- Create a Vue component with a contenteditable div bound to reactive data.
- Type some text and place cursor in the middle.
- Trigger a reactive state update that causes re-render.
- Observe that caret position jumps to the beginning.
Observed behavior
- Caret jumps: Caret position reverts to start of element on re-render
- Safari/Firefox: More prevalent in Safari and Firefox
- v-model doesn’t work: v-model is designed for form inputs, not contenteditable
- Event timing: change events don’t fire reliably, requiring input events
- Re-render frequency: Every keystroke can trigger watchers and re-renders
Expected behavior
- Caret position should be preserved during re-renders
- DOM updates should not reset cursor position
- Editing experience should remain smooth during state updates
Analysis
Vue’s reactivity system causes the component to re-render when data changes, which replaces DOM nodes and causes the browser to lose track of the caret position. Safari and Firefox handle DOM updates differently from Chrome, making them more susceptible to this issue.
Workarounds
- Use refs and manual DOM updates instead of reactive binding
- Save and restore caret position before and after DOM updates
- Debounce or throttle state updates to reduce re-render frequency
- Use custom component that handles caret preservation automatically