Scenario

selection.addRange not working correctly in Safari

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.

selection
Scenario ID
scenario-selection-addrange-safari-fail

Details

When using window.getSelection().addRange() to set cursor position in a contenteditable element, Safari exhibits incorrect behavior that breaks the intended selection placement.

Problem Description

This issue occurs when:

  1. A contenteditable element contains nested elements (e.g., <span> markers)
  2. Application tries to set cursor position programmatically using selection.addRange()
  3. Specifically when trying to position cursor within or near a specific element

Expected Behavior

  • Cursor (caret) should be positioned at the intended location within the specified element
  • Selection should remain within the intended container
  • Behavior should match Chrome and Firefox

Actual Behavior (Safari Bug)

  • Selection pops out: Instead of staying within the intended element, selection jumps to the next sibling’s text node
  • Wrong container: Selection ends up in a different element than intended
  • Inconsistent: Behavior differs from Chrome and Firefox

Affected Browsers

  • Safari (all versions) - Issue confirmed, WebKit bug
  • Chrome - Works correctly
  • Firefox - Works correctly

Root Cause

Safari’s WebKit selection API has a known issue with nested elements and addRange():

  1. When a range is created pointing to a location within a nested element
  2. Safari may incorrectly calculate the selection boundary
  3. Instead of respecting the intended container, Safari moves selection to an adjacent text node

This is particularly problematic when:

  • Working with markers or placeholders (e.g., <span> tags with specific IDs)
  • Trying to set cursor after programmatic DOM changes
  • Using selection.addRange() instead of selection.collapse()

Workarounds

  1. Use selection.collapse() with node and offset:

    const range = document.createRange();
    range.setStart(textNode, offset);
    range.collapse(true); // true = collapse to end
    const selection = window.getSelection();
    selection.removeAllRanges();
    selection.addRange(range);
  2. Use setTimeout with selection manipulation:

    setTimeout(() => {
      const range = document.createRange();
      range.setStart(element, 0);
      range.collapse(true);
      const selection = window.getSelection();
      selection.removeAllRanges();
      selection.addRange(range);
    }, 10);
  3. Force focus on the element first:

    targetElement.focus();
    setTimeout(() => {
      // Now try setting selection
      const range = document.createRange();
      range.setStart(targetElement, 0);
      const selection = window.getSelection();
      selection.removeAllRanges();
      selection.addRange(range);
    }, 0);
  4. Avoid nested elements:

    • Simplify DOM structure to avoid deep nesting
    • Use data attributes or class-based markers instead of separate span elements

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-0255-selection-addrange-chrome-en Windows 10/11 Desktop Any Chrome 120+ English (QWERTY) confirmed
ce-0256-selection-addrange-edge-en Windows 10/11 Desktop Any Edge (Chromium-based) 120+ English (QWERTY) confirmed
ce-0545-selection-addrange-firefox-en Windows 10/11 Desktop Any Firefox 120+ English (QWERTY) confirmed

Browser compatibility

This matrix shows which browser and OS combinations have documented cases for this scenario. Click on a cell to view the specific case.

Confirmed
Draft
No case documented

Cases

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

Related Scenarios

Other scenarios that share similar tags or category.

Tags: safari, webkit, cursor

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: selection, cursor

Cursor disappears with contenteditable="false" elements

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.

0 cases
Tags: selection, cursor

Selection restoration after DOM manipulation is unreliable

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.

5 cases

Comments & Discussion

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