beforeinput fires with insertCompositionText but input fires with deleteContentBackward
OS: iOS 17.0 · Device: iPhone or iPad Any · Browser: Safari 17.0 · Keyboard: Korean (IME)
Open case →Scenario
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.
During IME composition or in certain browser/IME combinations, the beforeinput event may have a different inputType than the corresponding input event. This mismatch can cause handlers to misinterpret the actual DOM change and requires storing beforeinput’s targetRanges for use in input event handling.
When composing text with an IME, the following mismatch can occur:
beforeinput event fires with inputType: 'insertCompositionText'
e.isComposing === truee.getTargetRanges() returns ranges indicating where composition text will be insertede.data contains the composition textinput event fires with inputType: 'deleteContentBackward' (or other different type)
inputType doesn’t match what was indicated in beforeinpute.data may be null or different from beforeinput.datainputType to determine what happened will get incorrect informationtargetRanges from beforeinput are crucial for understanding what actually changed, but they’re not available in input eventsinputType values will failinputType between beforeinput and input during compositioninputType mismatches, especially on iOSStore targetRanges from beforeinput events and use them in input event handlers:
let lastBeforeInputTargetRanges = null;
let lastBeforeInputType = null;
let lastBeforeInputData = null;
element.addEventListener('beforeinput', (e) => {
// Store targetRanges, inputType, and data for use in input handler
lastBeforeInputTargetRanges = e.getTargetRanges?.() || [];
lastBeforeInputType = e.inputType;
lastBeforeInputData = e.data;
// Handle beforeinput normally
if (e.inputType === 'insertCompositionText') {
// Prepare for composition text insertion
}
});
element.addEventListener('input', (e) => {
// Check for inputType mismatch
if (lastBeforeInputType && e.inputType !== lastBeforeInputType) {
console.warn('inputType mismatch:', {
beforeinput: lastBeforeInputType,
input: e.inputType,
beforeinputData: lastBeforeInputData,
inputData: e.data
});
// Use targetRanges from beforeinput to understand actual change
if (lastBeforeInputTargetRanges && lastBeforeInputTargetRanges.length > 0) {
// The targetRanges indicate what was actually changed
// Process based on targetRanges rather than inputType
handleActualChange(lastBeforeInputTargetRanges, e);
}
} else {
// Normal case: inputType matches
handleInput(e);
}
// Clear stored values after processing
lastBeforeInputTargetRanges = null;
lastBeforeInputType = null;
lastBeforeInputData = null;
});
function handleActualChange(targetRanges, inputEvent) {
// Reconstruct what actually happened using targetRanges
for (const range of targetRanges) {
// Convert StaticRange to Range for inspection
const actualRange = document.createRange();
actualRange.setStart(range.startContainer, range.startOffset);
actualRange.setEnd(range.endContainer, range.endOffset);
// Check what's in the range now vs what was there before
// This tells you the actual change regardless of inputType
}
}
Important Notes:
targetRanges are only available in beforeinput events, not in input eventstargetRanges are StaticRange objects that may become invalid after DOM changesinputType - always verify with DOM inspectiontargetRanges from beforeinput for use in input handlersbeforeinput.inputType matches input.inputTypeinputType is always correct - have fallback logicVisual 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-0213-ime-beforeinput-insertcompositiontext-input-deletecontentbackward | iOS 17.0 | iPhone or iPad Any | Safari 17.0 | Korean (IME) | draft |
Open a case to see the detailed description and its dedicated playground.
OS: iOS 17.0 · Device: iPhone or iPad Any · Browser: Safari 17.0 · Keyboard: Korean (IME)
Open case →Other scenarios that share similar tags or category.
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.
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.
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.
Analysis of how out-of-order or missing composition events disrupt editor state synchronization.
Have questions, suggestions, or want to share your experience? Join the discussion below.