What is an IME?
An Input Method Editor (IME) is a software component that allows users to enter characters that are not directly available on their keyboard. Common examples include:
- Japanese IME: Converts romanized input (romaji) into hiragana, katakana, or kanji
- Korean IME: Combines individual jamo characters into complete syllables
- Chinese IME: Converts pinyin or other phonetic input into Chinese characters
- Vietnamese IME: Adds diacritical marks to base characters
Korean IME Variations
Korean IMEs can use different keyboard layouts and composition methods, which can affect how composition events fire and how text is composed:
Keyboard Layout Types
2벌식 (두벌식) - Most Common
- Standard Korean keyboard layout
- Most widely used in South Korea
- Jamo characters arranged in two rows
- Example: ㅎ (H), ㅏ (A), ㄴ (N) → 한
3벌식 (세벌식)
- Alternative keyboard layout
- Jamo characters arranged in three rows
- Different key positions for same jamo
- May affect composition event timing
390 자판
- Another alternative layout
- Less commonly used
- May have different composition behavior
Composition Methods
조합형 (Composition-based)
- Real-time composition: ㅎ + ㅏ + ㄴ → 한
- Each jamo input triggers
compositionupdate - Composition text changes with each keystroke
- Most common method in modern Korean IMEs
완성형 (Completion-based)
- User selects from pre-composed characters
- May have different event patterns
- Less common in modern IMEs
⚠️ Impact on Composition Events
Different keyboard layouts and composition methods can affect:
- The number of
compositionupdateevents fired - The timing of when composition text is updated
- How selection ranges change during composition
- Whether
beforeinputevents fire for formatting shortcuts
Example: macOS Korean IME with 2벌식 may behave differently than Windows Korean IME with 3벌식, even for the same text input.
Composition events
When using an IME, the input process involves multiple stages tracked by composition events:
-
compositionstart– Fires when the user starts composing (e.g., begins typing in Japanese) -
compositionupdate– Fires repeatedly as the composition changes (e.g., as candidate characters appear) -
compositionend– Fires when the composition is finalized (e.g., user selects a kanji)
element.addEventListener('compositionstart', () => {
console.log('Composition started');
});
element.addEventListener('compositionupdate', (e) => {
console.log('Composing:', e.data);
});
element.addEventListener('compositionend', (e) => {
console.log('Composition ended:', e.data);
});Composition Event Timing Variations
The exact timing and occurrence of compositionstart, compositionupdate, and compositionend events can vary significantly depending on multiple factors:
Factors Affecting Composition Event Timing
1. Keyboard/IME Type
- Korean IME: 2벌식 vs 3벌식 vs 390 자판 may fire events at different points
- Japanese IME: Different IME engines (Microsoft IME, Google IME, etc.) may have different timing
- Chinese IME: Pinyin vs other input methods may trigger events differently
- Mobile keyboards: Gboard, Samsung Keyboard, iOS QuickType all have different composition behaviors
2. Browser
- Chrome/Edge: May fire
compositionupdatebeforebeforeinputin some cases - Firefox: Different event ordering and timing
- Safari: May have unique composition event patterns
- Event order can change between browser versions
3. Operating System
- macOS: Uses system-level IME, may have different timing than Windows
- Windows: IME behavior varies by Windows version and IME provider
- Linux: IME implementation varies by distribution and desktop environment
- Android: OS-level keyboard integration affects event timing
- iOS: System keyboard behavior differs from desktop
4. Device Type
- Desktop: Physical keyboard input may trigger events differently
- Mobile: Virtual keyboard with text prediction may skip or delay events
- Tablet: Hybrid behavior between desktop and mobile
Example: Korean IME Timing Differences
macOS + Chrome + Korean IME (2벌식)
Windows + Firefox + Korean IME (3벌식)
Android + Samsung Keyboard + Text Prediction
⚠️ Critical: Unpredictable Event Timing
Composition event timing is NOT guaranteed to be consistent:
compositionstartmay fire before, after, or simultaneously withbeforeinputcompositionupdatemay fire multiple times per keystroke or skip keystrokescompositionendmay fire immediately after commit or be delayed- Some IMEs may not fire
compositionstartat all in certain scenarios - Mobile keyboards with text prediction may skip composition events entirely
Best Practice: Never rely on exact event timing. Always check the isComposing flag and DOM state to determine composition status.
Detecting Composition State Reliably
Instead of relying on event timing, use these approaches:
- Check
isComposingflag: Available onbeforeinputandinputevents - Track composition state: Set a flag on
compositionstart, clear oncompositionend - Compare DOM state: Check if composition text exists in DOM between events
- Handle missing events: Don't assume
compositionstartwill always fire - Test across platforms: Test with different keyboards, browsers, and devices
⚠️ Important: Store targetRanges from beforeinput
When beforeinput and input have different inputType values:
- The
targetRangesfrombeforeinputare crucial for understanding actual DOM changes targetRangesare only available inbeforeinputevents, not ininputevents- Always store
targetRangesfrombeforeinputfor use ininputhandlers - Compare
inputTypevalues between events to detect mismatches
// Store targetRanges from beforeinput
let lastBeforeInputTargetRanges = null;
let lastBeforeInputType = null;
element.addEventListener('beforeinput', (e) => {
// Store targetRanges, inputType for use in input handler
lastBeforeInputTargetRanges = e.getTargetRanges?.() || [];
lastBeforeInputType = e.inputType;
});
element.addEventListener('input', (e) => {
// Check for inputType mismatch
if (lastBeforeInputType && e.inputType !== lastBeforeInputType) {
// Use targetRanges from beforeinput to understand actual change
if (lastBeforeInputTargetRanges && lastBeforeInputTargetRanges.length > 0) {
// Process based on targetRanges rather than inputType
handleActualChange(lastBeforeInputTargetRanges, e);
}
}
// Clear stored values
lastBeforeInputTargetRanges = null;
lastBeforeInputType = null;
});Selection During Composition
During IME composition, the selection behavior is unique: the selection range changes with each compositionupdate, but visually, only a single cursor is shown.
How Selection Works During Composition
1. Composition Text in DOM
During composition, the composition text is actually inserted into the DOM as text nodes. It's not just a visual overlay - it exists as real DOM content.
2. Selection Range Changes
As the user types during composition, the selection range expands to include the composition text:
3. Visual Cursor vs Selection Range
Despite the selection range including the composition text, visually only one cursor is shown at the end of the composition text. The composition text itself is typically displayed with an underline or highlight to indicate it's temporary.
ℹ️ Browser Internal Management
Who manages the composition selection?
- Browser: The browser manages the composition selection internally. It creates and updates a Range that includes the composition text.
- IME/Keyboard: The IME (Input Method Editor) communicates with the browser to indicate where composition text should be placed, but the browser is responsible for managing the actual DOM and selection.
- Selection API:
window.getSelection()returns the current selection, which includes the composition text during composition.
The browser maintains a composition range that is separate from the user's visible selection, but is accessible through the Selection API.
Example: Selection Changes During Composition (2벌식 Korean IME)
User types "ㅎ" (first jamo)
User types "ㅏ" (second jamo) → "하"
User types "ㄴ" (third jamo) → "한"
User commits (Space/Enter) → Composition ends
ℹ️ Note: Keyboard Layout Differences
Different Korean keyboard layouts (2벌식, 3벌식, 390) may behave differently:
- The number of
compositionupdateevents may vary - The composition text at each step may differ
- Selection range behavior may be slightly different
- Event timing may vary between layouts
Always test with different keyboard layouts if your application needs to support various Korean input methods.
⚠️ Important for Editors
When editors use preventDefault() and manipulate DOM:
- The browser's composition selection may become invalid
- If you re-render DOM from your internal model, the composition text may be lost
- The browser's composition range may not match your model's state
- You may need to manually restore the selection after DOM updates
Best Practice: During composition, avoid programmatic DOM updates that would interfere with the browser's composition management. Let the browser handle composition text insertion, then sync with your model after compositionend.
Common issues
Enter key during composition
Pressing Enter during IME composition may cancel the composition instead of committing it. This breaks the expected workflow for users.
See cases: scenario-ime-enter-breaks
Backspace granularity
Backspace may delete entire composed syllables instead of individual characters, making fine-grained editing difficult.
See cases: scenario-ime-backspace-granularity
Space key during composition
The Space key may be ignored or commit composition inconsistently during IME input, depending on the browser and OS.
See cases: scenario-space-during-composition
Undo during composition
Undo operations during active composition may clear more text than expected, including text that was already committed.
See cases: scenario-undo-with-composition
Duplicate keydown events with keyCode 229
During IME composition, pressing certain keys (especially Enter) may trigger duplicate keydown events. The first event has keyCode: 229 (indicating IME is processing the input), followed by the actual key's keyCode (e.g., 13 for Enter). This can cause event handlers to execute twice for a single key press.
See cases: scenario-ime-composition-keydown-keycode-229
beforeinput and input inputType mismatch
During IME composition, 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 requires storing beforeinput's targetRanges for use in input event handling.
Mobile Keyboards & Text Prediction
Mobile keyboards differ significantly from desktop keyboards, especially when text prediction/suggestion features are enabled. Different keyboard apps (Gboard, Samsung Keyboard, iOS QuickType, etc.) may behave differently.
Mobile Keyboard Types
Android Keyboards
- Gboard (Google Keyboard): Google's keyboard with text prediction
- Samsung Keyboard: Samsung's default keyboard with phrase suggestion
- SwiftKey: Third-party keyboard with advanced prediction
- Each may use different inputType values and event patterns
iOS Keyboards
- iOS QuickType: Apple's predictive text feature
- Third-party keyboards: Various keyboards available in App Store
- Generally more predictable than Android keyboards
⚠️ Text Prediction/Suggestion Features
When text prediction is enabled:
beforeinputmay not fire before suggestion insertion- Multiple
inputevents may fire for a single suggestion selection - Suggested text may replace more content than expected (entire words/phrases)
- Event timing differs significantly from manual typing
- Selection ranges in
beforeinputandinputmay differ
Impact: Text prediction can interfere with custom editing logic, undo/redo stacks, and change tracking systems.
Selection Differences: beforeinput vs input
Why Selection Differs
beforeinput Event
- Fires before DOM changes occur
- Selection range reflects the current state (before text insertion/replacement)
- May show the range that will be replaced by the suggestion
- In text prediction scenarios, may not fire at all
input Event
- Fires after DOM changes occur
- Selection range reflects the new state (after text insertion/replacement)
- Shows cursor position after the inserted/replaced text
- May fire multiple times for a single suggestion (bulk insertion)
Example: Text Prediction with Selection Differences
Scenario: User types "안녕", keyboard suggests "안녕하세요"
User selects suggestion "안녕하세요"
⚠️ Critical: Selection Mismatch
The selection in beforeinput and input events can be completely different:
beforeinputselection shows what will be replacedinputselection shows the final cursor position- If
beforeinputdoesn't fire, you only see theinputselection - Multiple
inputevents may have different selections as text is inserted in chunks
Best Practice: Always compare DOM state before and after events to understand what actually changed, rather than relying solely on selection ranges.
Handling Mobile Text Prediction
- Monitor both events: Listen to both
beforeinputandinputto catch all text insertions - Compare DOM states: Store DOM state before events and compare after to detect bulk insertions
- Don't rely on selection alone: Selection ranges may not accurately reflect what was inserted/replaced
- Handle multiple input events: A single suggestion may trigger multiple
inputevents - Consider disabling prediction: Use
autocomplete="off"orspellcheck="false"if it interferes with your use case
Browser differences
Event timing: The timing and order of composition events varies significantly between browsers, especially when special keys (Enter, Backspace) are pressed during composition.
IME candidate window: The position and behavior of the IME candidate window (showing possible character conversions) differs across browsers and may not align correctly with the caret.
Composition events missing: Some IMEs may not fire composition events consistently, or events may fire in unexpected orders, making it difficult to track the composition state.
Selection range behavior: How the selection range is managed during composition may differ between browsers. Some browsers may show the composition text as selected, while others only show a cursor.
Composition Selection & Range Management
Technical Details: How Composition Selection Works
Browser's Internal Composition Range
The browser maintains a composition range that is separate from the user's visible selection:
- During composition, the browser creates a Range that includes the composition text
- This range is updated on each
compositionupdateevent - The Range's start and end positions change as composition text changes
- The Range is accessible via
window.getSelection().getRangeAt(0)
Visual vs Actual Selection
Even though the selection range includes the composition text (making it a non-collapsed selection), the browser renders it as a single cursor:
- The composition text is visually highlighted/underlined to indicate it's temporary
- The cursor appears at the end of the composition text
- The selection range spans the entire composition text, but this is not visually shown as a selection
Why Selection Changes But Cursor Doesn't
The selection range changes because:
- Each
compositionupdatereplaces the previous composition text with new text - The Range's boundaries are updated to match the new composition text
- The Range's
toString()returns the current composition text - But visually, the browser only shows a cursor (not a selection highlight) because it's composition text
Code Example: Observing Selection During Composition
// Monitor selection changes during composition
element.addEventListener('compositionupdate', (e) => {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
console.log('Composition text:', e.data);
console.log('Selection range:', {
start: range.startOffset,
end: range.endOffset,
text: range.toString(),
collapsed: range.collapsed
});
// Note: range.toString() returns the composition text
// range.collapsed may be false even though cursor looks like one position
}
});
// Compare with compositionend
element.addEventListener('compositionend', (e) => {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
console.log('After composition:', {
start: range.startOffset,
end: range.endOffset,
text: range.toString(),
collapsed: range.collapsed
});
// Now range is typically collapsed at the end of committed text
}
});