Scenario

getTargetRanges() can reference the wrong contenteditable when editors are nested

On some engines (notably Firefox for Android / Fenix), beforeinput getTargetRanges() may describe the outer contenteditable host instead of the inner focused editor. Custom handlers that trust targetRanges alone may delete or insert in the parent surface while the user believes they are typing in a nested field.

input
Scenario ID
scenario-gettargetranges-nested-wrong-scope

Details

Nested contenteditable layouts (outer page shell with contenteditable="true" and an inner contenteditable="true" for a caption, comment box, or embedded widget) are fragile. Even when focus and the visible caret are inside the inner host, InputEvent.getTargetRanges() may still return a StaticRange spanning the outer editable root on certain builds. That is a different failure mode from “empty targetRanges”: the array is non-empty but semantically wrong, so fallback-to-selection and trust-targetRanges strategies both break in opposite ways.

Problem Overview

The Input Events specification assumes getTargetRanges() reflects the DOM ranges that will be modified by the incoming edit. With nested editables, the editing engine must disambiguate which host owns the current editing session. When the engine attaches the range to the outer host, beforeinput listeners that call preventDefault() and apply their own mutation using targetRanges[0] will corrupt the outer document—while keyboard input still visually appears tied to the inner field in some steps.

Observed Behavior

  • Fenix (Firefox for Android): Reported behavior: nested inner contenteditable receives keystrokes but getTargetRanges() points at the parent range; text can be applied to the wrong tree location relative to editor expectations.
  • Desktop Firefox / Chrome: May behave correctly for simple nesting; still verify for shadow DOM, contenteditable="plaintext-only", and custom wrappers.
outer.addEventListener('beforeinput', (e) => {
  const tr = e.getTargetRanges?.() ?? [];
  if (tr.length) {
    const { startContainer } = tr[0];
    console.log('range root editable:', startContainer.parentElement?.closest('[contenteditable]'));
  }
});

Impact

  • Wrong delete/insert span: deleteContentBackward uses a range that removes parent content instead of the inner field.
  • Collaborative / OT bugs: Operational transforms keyed off bad ranges duplicate or drop characters.
  • Security-adjacent UX: Less common, but surprising mutations in the outer shell are hard to undo from the user’s mental model.

Browser Comparison

  • Firefox Android (Fenix): Community reports of parent-scoped getTargetRanges() with nested editables; treat as high risk until verified fixed.
  • Blink / WebKit (typical desktop): Often correct for simple nesting; still test mobile WebKit.
  • Empty vs wrong: Contrast with scenario-gettargetranges-empty, where the array is empty and you must fall back to selection.

Solutions

  1. Validate range against focused node: Before applying targetRanges[0], require focusedEditable.contains(startContainer) where focusedEditable is your innermost contenteditable="true" that matches document.activeElement or shadow-aware active element.

  2. Prefer active editable host: If targetRanges and getSelection() disagree, prefer the range whose commonAncestorContainer is contained in the same host as document.activeElement.

  3. Avoid nested contenteditable: Where possible, use a single outer host and inner contenteditable="false" islands with programmatic editing, or isolate nested fields in iframes (trade-offs apply).

  4. Feature-detect bad coupling: On Firefox Android, run a one-time manual test harness logging activeElement, getSelection(), and getTargetRanges() on a single keypress; cache a “do not trust targetRanges for nested” flag per UA if needed.

Best Practices

  • Never treat non-empty targetRanges as automatically correct when multiple editables nest.
  • Log both selection and targetRanges when debugging mobile Firefox reports.
  • Track upstream issues; engine fixes can narrow the workaround surface.

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-0586-firefox-android-nested-gettargetranges-parent-range Android 13+ Phone Any Firefox 120+ Gboard / stock 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: getTargetRanges, beforeinput, android

getTargetRanges() returns empty array in beforeinput events

The getTargetRanges() method in beforeinput events may return an empty array or undefined in various scenarios, including text prediction, certain IME compositions, or specific browser/device combinations. When getTargetRanges() is unavailable, developers must rely on window.getSelection() as a fallback, but this may be less accurate.

1 case
Tags: android, mobile

Typing certain characters makes cursor jump on Chrome Mobile

On Chrome Mobile for Android, typing certain punctuation characters (commas, colons, semicolons, quotes, etc.) in the middle of a word causes the cursor to jump to the end of the word instead of staying at the insertion point.

2 cases
Tags: android, mobile

Input Events Fire on Focus/Blur in Chrome Android

In Chrome on Android, input events may fire when a contenteditable element gains or loses focus, even without content changes. This behavior can lead to unintended side effects in applications relying on input events for content modification detection.

1 case
Tags: nested, firefox

contenteditable inheritance behavior is inconsistent

When a parent element has contenteditable="true" and a child element has contenteditable="false", the inheritance behavior is inconsistent across browsers. Some browsers allow editing in the child, while others correctly prevent it. The behavior may also differ when the child has contenteditable="inherit" or no contenteditable attribute.

1 case

Comments & Discussion

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