Phenomenon
When pressing Enter during Korean IME composition in a contenteditable element, two keydown events fire sequentially:
- First event with
keyCode: 229(indicating IME is processing the input) - Second event with
keyCode: 13(the actual Enter key)
This can cause event handlers listening to keydown events to execute twice for a single Enter key press.
Reproduction example
- Focus a
contenteditableelement. - Activate Korean IME.
- Start composing Korean text (e.g., type “ㅎ” then “ㅏ” then “ㄴ” to compose “한”).
- While composition is active, press Enter.
- Observe
keydownevents in the browser console or event log.
Observed behavior
When pressing Enter during composition:
-
First
keydownevent:keyCode: 229(special value indicating IME processing)key: 'Process'or similar IME-related value- May have
isComposingproperty (browser-dependent) - The actual key code is not available at this point
-
Second
keydownevent:keyCode: 13(Enter key)key: 'Enter'- Occurs after IME processes the input
-
Result:
- Event handlers listening to
keydownwithkeyCode === 13execute twice - This can cause duplicate line breaks, duplicate command execution, or other unintended behavior
- Event handlers listening to
Expected behavior
- Only one
keydownevent should fire for a single Enter key press - If
keyCode 229fires, it should be clearly distinguishable from the actual key event - Event handlers should not need special logic to deduplicate key events during composition
- The
keyCode 229event should not trigger handlers intended for the actual key
Impact
This can lead to:
- Duplicate line breaks: Enter key handler creates two
<br>elements instead of one - Duplicate command execution: Keyboard shortcuts or commands execute twice
- Performance issues: Event handlers process the same action twice
- State synchronization issues: Application state may become inconsistent
- Unexpected UI behavior: UI may respond twice to a single user action
Browser Comparison
- Chrome/Edge: Fires
keyCode 229followed by actualkeyCode 13during composition - Firefox: May have similar behavior
- Safari: Behavior may vary
Notes and possible direction for workarounds
-
Check for
keyCode 229: Ignorekeydownevents withkeyCode 229during composition:let isComposing = false; element.addEventListener('compositionstart', () => { isComposing = true; }); element.addEventListener('compositionend', () => { isComposing = false; }); element.addEventListener('keydown', (e) => { if (isComposing && e.keyCode === 229) { // Ignore keyCode 229 events during composition return; } // Handle actual key events if (e.keyCode === 13) { // Handle Enter key } }); -
Use
beforeinputevents: Consider usingbeforeinputevents instead ofkeydownwhen possible, as they provide better composition state information:element.addEventListener('beforeinput', (e) => { if (e.isComposing) { // Handle composition-related input return; } if (e.inputType === 'insertParagraph') { // Handle Enter key (line break) } }); -
Track last keyCode 229: If you need to handle both events, track the sequence:
let isComposing = false; let lastKeyCode229 = null; element.addEventListener('keydown', (e) => { if (isComposing) { if (e.keyCode === 229) { lastKeyCode229 = e; return; // Ignore keyCode 229 } // If actual keyCode follows keyCode 229, handle it once if (lastKeyCode229 && e.keyCode === 13) { lastKeyCode229 = null; // Handle Enter key once handleEnter(); return; } } }); -
Important:
keyCodeis deprecated. Consider usinge.keyore.codeinstead when possible, but note thatkeyCode 229is a special case that may still be relevant for IME handling.