Firefox Android may report parent contenteditable range in beforeinput for nested inner editor
OS: Android 13+ · Device: Phone Any · Browser: Firefox 120+ · Keyboard: Gboard / stock
Open case →Scenario
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.
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.
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.
contenteditable receives keystrokes but getTargetRanges() points at the parent range; text can be applied to the wrong tree location relative to editor expectations.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]'));
}
});
deleteContentBackward uses a range that removes parent content instead of the inner field.getTargetRanges() with nested editables; treat as high risk until verified fixed.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.
Prefer active editable host: If targetRanges and getSelection() disagree, prefer the range whose commonAncestorContainer is contained in the same host as document.activeElement.
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).
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.
targetRanges as automatically correct when multiple editables nest.targetRanges when debugging mobile Firefox reports.targetRanges and fallback strategiesVisual view of how this scenario connects to its concrete cases and environments. Nodes can be dragged and clicked.
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 |
Open a case to see the detailed description and its dedicated playground.
OS: Android 13+ · Device: Phone Any · Browser: Firefox 120+ · Keyboard: Gboard / stock
Open case →Other scenarios that share similar tags or category.
Using the HTML drag-and-drop API inside or alongside contenteditable regions often diverges from behavior on plain elements: default actions, `contenteditable` hit-testing, and `beforeinput`/`drop` ordering differ by browser. Custom editors must reconcile native DnD with their own selection model.
Firefox may hide or misplace the caret when moving across contenteditable=true/false boundaries—widgets and embeds inside editors are affected.
In Chromium, programmatic DOM updates (normalization, wrapping, React reconciliation) while the user is typing can move the caret to the end of the contenteditable or to an unexpected boundary—especially when the mutation happens between keystrokes.
Dragging selected content inside a contenteditable region on Firefox can duplicate nodes or leave ghost fragments—different from Chrome's behavior.
Firefox may not show a caret in an empty block-level contenteditable container until the user types or a br placeholder exists—layout and min-height interact with focus rings.
Have questions, suggestions, or want to share your experience? Join the discussion below.