iOS Safari — forced re-render when inputType is undefined/null or during multi insertText breaks model–DOM sync
OS: iOS 17 · Device: Phone iPhone 15 · Browser: Safari 17 · Keyboard: US QWERTY / Voice dictation
Open case →Scenario
On iOS Safari, input and beforeinput can fire with inputType 'insertText' multiple times (e.g. voice dictation) or with inputType undefined/null. Forcing re-render or changing selection during this flow desyncs the editor model from the DOM and can permanently break subsequent input.
On iOS Safari, when the user types or uses voice dictation in a contenteditable element, the editor must observe input and update its internal model only. It must not force a re-render (e.g. writing back DOM from the model) or programmatically change the selection in the middle of the input stream. Reasons:
beforeinput / input with inputType: 'insertText' multiple times (e.g. word by word or in chunks). If the editor re-renders or moves the selection after the first event, the following events apply to the wrong place and the model and DOM go out of sync.event.inputType is not guaranteed. When it is undefined or null, the browser is still applying a change. If the editor treats “unknown inputType” as a signal to force-sync or re-render from the model, it overwrites or misaligns the DOM. After that, all subsequent input appears “broken”: the model can no longer be kept in sync with the DOM.The safe pattern is: observe input, update the model from the DOM (or from the event), and do not write DOM or selection back during the input flow.
insertText events. After the first one, the DOM already contains part of the dictated text; the next event carries more text. If the editor re-renders (e.g. React setState → DOM replace) or restores selection after the first event, the second and later events apply to stale or wrong positions and the final text is wrong or duplicated.input or beforeinput fires with event.inputType === undefined or null. The DOM is still updated by the browser. If the editor does something like “if (!inputType) force sync from model to DOM” or “if (!inputType) restore selection”, it overwrites the browser’s change or moves the caret. From that point on, the model and DOM diverge and further typing/dictation appears broken.selection.removeAllRanges() / addRange() or otherwise changing the selection during the input stream (e.g. after each input) has the same effect as re-rendering: the next event applies at the wrong place and sync is lost.input / beforeinput handlers, only read from the DOM (or from the event) and update the editor model. Do not write the model back to the DOM and do not change the Selection during the same tick (or until the input “burst” is over).event.inputType is undefined or null, do not treat it as “unknown, so overwrite DOM from model”. Treat it as “browser applied a change; only read and update model”. Avoid any path that does force-sync or re-render when inputType is falsy.Example: avoid forced re-render when inputType is missing:
editable.addEventListener('input', (e) => {
// Only read and update model; do not write back to DOM here
const newContent = editable.innerHTML; // or get from e / getTargetRanges
updateModel(newContent);
// Do NOT do this when inputType is undefined/null (or during insertText stream):
// setState(newContent); // → React re-renders → DOM replaced → desync on iOS
// selection.removeAllRanges(); selection.addRange(myRange); // → next input at wrong place
});
inputType can be undefined or null; never use “missing inputType” as a reason to force DOM or selection update from the model.insertText events in a row (e.g. voice); do not re-render or change selection between them.Visual 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-0584-ios-safari-inputtype-null-forced-render-breaks-sync | iOS 17 | Phone iPhone 15 | Safari 17 | US QWERTY / Voice dictation | draft |
Open a case to see the detailed description and its dedicated playground.
OS: iOS 17 · Device: Phone iPhone 15 · Browser: Safari 17 · Keyboard: US QWERTY / Voice dictation
Open case →Other scenarios that share similar tags or category.
On iOS, when using voice dictation to input text into contenteditable elements, the system may fire duplicate beforeinput and input events after the initial dictation completes. The text is split into words and events are re-fired, causing synchronization issues. Composition events do not fire during dictation, making it difficult to distinguish dictation from keyboard input.
The selection (window.getSelection()) in beforeinput events can differ from the selection in corresponding input events. This mismatch can occur during IME composition, text prediction, or when typing adjacent to formatted elements like links. The selection in beforeinput may include adjacent formatted text, while input selection reflects the final cursor position.
During IME composition or in certain browser/IME combinations, the beforeinput event may have a different inputType than the corresponding input event. For example, beforeinput may fire with insertCompositionText while input fires with deleteContentBackward. This mismatch can cause handlers to misinterpret the actual DOM change and requires storing beforeinput's targetRanges for use in input event handling.
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.
In Safari desktop, when preventDefault() is called on keydown or beforeinput events for insertParagraph (Enter key), the IME composition state becomes corrupted. Subsequent text input fails to trigger proper input events, causing characters to not be inserted or composition to malfunction.
Have questions, suggestions, or want to share your experience? Join the discussion below.