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.
Some browsers and keyboards emit duplicate composition-related input or beforeinput events—especially iOS Safari dictation paths and certain Android keyboards—so naive handlers that insert text on every input may double characters or corrupt state.
Some beforeinput events during IME composition cannot be canceled per spec or implementation—calling preventDefault may throw or be ignored, so editors cannot always block native insertion.
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.
On some iOS Safari versions and keyboards, compositionstart or compositionupdate may not fire reliably for certain languages, while input events still fire—editors that only reconcile on composition boundaries can desync.
Have questions, suggestions, or want to share your experience? Join the discussion below.