Scenario

Caret position jumps to beginning on React re-render

When using contentEditable elements in React, the caret (cursor) position jumps to the beginning of the element whenever the component re-renders. This occurs because React's reconciliation process replaces DOM nodes, causing the browser to lose track of the caret position. This issue is more prevalent in Safari and Firefox.

other
Scenario ID
scenario-react-caret-jumps-on-rerender

Details

When using contentEditable elements in React, the caret (cursor) position jumps to the beginning of the element whenever the component re-renders. This occurs because React’s reconciliation process replaces DOM nodes, causing the browser to lose track of the caret position.

Observed Behavior

  • Caret jumps to start: Caret position reverts to the beginning of the element on every re-render
  • Safari/Firefox: More prevalent in Safari and Firefox due to their DOM update handling
  • DOM replacement: React replaces DOM nodes during re-render, losing caret position
  • State updates: Any state change that triggers re-render causes the issue
  • User experience: Severely disrupts typing flow and editing experience

Root Cause

React’s reconciliation algorithm may replace DOM nodes when state changes, causing 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.

Browser Comparison

  • Safari: Most affected - caret jumps frequently on re-render
  • Firefox: Also affected - similar behavior to Safari
  • Chrome: Less affected but still occurs in some cases
  • Edge: Similar to Chrome

Impact

  • Poor user experience: Users cannot type continuously without interruption
  • Data loss risk: Users may lose their typing position and context
  • Framework limitation: Makes React integration with contenteditable challenging
  • Development overhead: Requires additional code to preserve caret position

Workarounds

1. Use Uncontrolled Components with Refs

Avoid controlled components and use refs to manage content:

import React, { useRef } from 'react';

function ContentEditable() {
  const contentRef = useRef(null);
  
  const handleInput = (e) => {
    // Handle input without triggering re-render
    const content = e.currentTarget.textContent;
    // Update state only when needed (e.g., on blur)
  };
  
  return (
    <div
      contentEditable
      ref={contentRef}
      onInput={handleInput}
      suppressContentEditableWarning
    />
  );
}

2. Preserve and Restore Caret Position

Save caret position before updates and restore after:

import React, { useRef, useEffect } from 'react';

function ContentEditable({ value, onChange }) {
  const editableRef = useRef(null);
  const caretPositionRef = useRef(null);
  
  const saveCaretPosition = () => {
    const selection = window.getSelection();
    if (selection.rangeCount > 0) {
      const range = selection.getRangeAt(0);
      caretPositionRef.current = {
        startContainer: range.startContainer,
        startOffset: range.startOffset,
        endContainer: range.endContainer,
        endOffset: range.endOffset
      };
    }
  };
  
  const restoreCaretPosition = () => {
    if (!caretPositionRef.current) return;
    const selection = window.getSelection();
    const range = document.createRange();
    range.setStart(
      caretPositionRef.current.startContainer,
      caretPositionRef.current.startOffset
    );
    range.setEnd(
      caretPositionRef.current.endContainer,
      caretPositionRef.current.endOffset
    );
    selection.removeAllRanges();
    selection.addRange(range);
  };
  
  useEffect(() => {
    if (editableRef.current && value !== editableRef.current.textContent) {
      saveCaretPosition();
      editableRef.current.textContent = value;
      restoreCaretPosition();
    }
  }, [value]);
  
  return (
    <div
      contentEditable
      ref={editableRef}
      onInput={(e) => onChange(e.currentTarget.textContent)}
      suppressContentEditableWarning
    />
  );
}

3. Use Specialized Libraries

Libraries that handle caret management automatically:

  • use-editable: React hook for contenteditable with caret preservation
  • react-contenteditable: Wrapper component that handles caret position
  • slate-react: Rich text editor framework built for React

4. Minimize Re-renders

Use React.memo, useMemo, and useCallback to prevent unnecessary re-renders:

const ContentEditable = React.memo(({ value, onChange }) => {
  // Component implementation
}, (prevProps, nextProps) => {
  // Only re-render if value actually changed
  return prevProps.value === nextProps.value;
});

References

Scenario flow

Visual view of how this scenario connects to its concrete cases and environments. Nodes can be dragged and clicked.

React Flow mini map

Variants

Each row is a concrete case for this scenario, with a dedicated document and playground.

Case OS Device Browser Keyboard Status
ce-0316-react-caret-jumps-on-rerender Any Any Desktop or Laptop Any Safari Latest US draft

Cases

Open a case to see the detailed description and its dedicated playground.

Related Scenarios

Other scenarios that share similar tags or category.

Tags: caret, safari, firefox

Text caret is invisible on position:relative elements

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.

2 cases
Tags: caret, firefox

Caret position jumps unexpectedly after pasting content

After pasting content into a contenteditable region, the caret position does not end up at the expected location, sometimes jumping to the beginning of the pasted content or to an unexpected position.

1 case
Tags: framework, caret

Framework state synchronization issues with contenteditable

When using contenteditable with JavaScript frameworks like Vue, Angular, or Svelte, state synchronization between the DOM and framework state can cause caret position issues, event mismatches, and performance problems. Each framework has unique challenges when integrating with contenteditable.

1 case
Tags: safari

beforeinput event is not supported in Safari

The beforeinput event, which is crucial for intercepting and modifying input before it's committed to the DOM, is not supported in Safari. This makes it difficult to implement custom input handling that works across all browsers.

1 case
Tags: caret

Dark mode causes caret visibility and styling issues

When browser dark mode is enabled, contenteditable elements may experience invisible or poorly visible caret, inline style injection conflicts, background color issues, and form control styling problems. These issues are caused by missing color-scheme declarations and conflicts between browser-injected styles and custom CSS.

1 case

Comments & Discussion

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