Phenomenon
A long-standing but actively documented (as of Sept 2025) bug in Safari causes a reversal of the expected event sequence during IME commit. According to UI Events specifications, the keydown event for the Enter key used to commit a composition should have isComposing: true and occur before the compositionend event. Safari incorrectly dispatches compositionend first, followed by a keydown where isComposing is false.
Reproduction Steps
- Open a
contenteditableregion in Safari (macOS or iOS). - Start an IME composition (e.g., Japanese, Korean, or Chinese).
- Type some characters so a composition underline appears.
- Press the Enter key once to finalize the composition.
- Log the sequence of
keydown,compositionupdate, andcompositionend.
Observed Behavior
compositionend: Fires immediately upon pressing Enter. Internal “composing” state is set tofalse.keydown: Fires afterwards.e.key: “Enter”e.isComposing:false(Mismatch!)
- Result: Applications that listen for “Enter” to perform actions (like sending a chat message or creating a new line) will execute that action because they believe the composition is already finished, even though this specific Enter press was intended only to finish the composition.
Expected Behavior
The keydown event should fire first with isComposing: true, allowing the application to call preventDefault() or ignore the keypress. Then, compositionend should fire to mark the end of the session.
Impact
- Premature Submission: Chat apps send an empty or incomplete message when the user only wanted to confirm a character.
- Double Newlines: The editor inserts a newline immediately after the committed text.
- State Corruption: Frameworks that expect a certain lifecycle are thrown off by the sudden termination of the composition session before the key event.
Browser Comparison
- Safari (all versions including 18.0): Exhibits the out-of-order behavior.
- Chrome / Firefox: Correctly fires
keydown(isComposing: true)beforecompositionend.
References & Solutions
Mitigation: keyCode 229 or Debouncing
Since isComposing is unreliable in Safari, developers often check for the special keyCode: 229 (IME process) or use a “lock” mechanism.
let isSafariIME = false;
element.addEventListener('compositionstart', () => { isSafariIME = true; });
element.addEventListener('compositionend', () => {
// Delay unlocking to catch the trailing keydown in Safari
setTimeout(() => { isSafariIME = false; }, 50);
});
element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && (e.isComposing || isSafariIME || e.keyCode === 229)) {
// Correctly identifies this Enter as a commit action
e.preventDefault();
}
});