15 merged IME fixes: the recurring ways Japanese typing breaks web apps
Between late June and early July 2026 I had 15 pull requests merged into open-source projects, all fixing the same family of bug: a keyboard handler that does not know an input method editor (IME) is in the middle of com
Between late June and early July 2026 I had 15 pull requests merged into open-source projects, all fixing the same family of bug: a keyboard handler that does not know an input method editor (IME) is in the middle of composing text. This post groups them into the bug classes that keep recurring, with a minimal repro and the fix pattern for each. Every fix links to the merged PR so you can read the real diff.
Background in one paragraph
Japanese, Chinese and Korean are typed through an IME. In Japanese you type the reading (toukyou), the IME shows candidates (ζ±δΊ¬), and you press Enter to confirm the candidate. That Enter is a real keydown event. While composition is active, KeyboardEvent.isComposing is true (MDN), and keydowns handled by the IME carry the special keyCode 229 (MDN keydown, which shows the guard if (event.isComposing || event.keyCode === 229) return;). Code that reads only event.key === "Enter" cannot tell "confirm this kanji" from "send this message".
Class 1: Enter sends the message mid-composition
What the user sees: they type a reading, press Enter to pick the kanji, and the chat sends half a sentence in romaji or unconverted kana.
Minimal repro:
textarea.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); }
});
Fix pattern:
textarea.addEventListener("keydown", (e) => {
if (e.isComposing || e.keyCode === 229) return;
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); }
});
Merged evidence:
- CopilotKit/CopilotKit #5764: Angular chat input submitted during composition.
-
misskey-dev/misskey #17646: chat sent while the IME was composing; the guard also checks
e.key === "Process". - twentyhq/twenty #22270: chat-thread input and attachment rename input.
- enricoros/big-AGI #1145: custom instruction field in the Diagrams view.
Class 2: Enter commits a partial value (tags, list items, renames, validated fields)
What the user sees: a tag chip, list item, card title or file name is saved as the unconverted reading, and the actual kanji they picked ends up as a second stray entry, or is lost. In a validated numeric or formatted field, the half-typed value is committed and validated.
Minimal repro:
input.addEventListener("keydown", (e) => {
if (e.key === "Enter" && input.value.trim()) addTag(input.value);
});
Fix pattern: the same guard, placed before any commit or validation runs.
input.addEventListener("keydown", (e) => {
if (e.isComposing || e.keyCode === 229) return;
if (e.key === "Enter" && input.value.trim()) addTag(input.value);
});
In React, several of these PRs (onyx, payload, rsuite, LibreChat) read e.nativeEvent.isComposing, because the handler receives React's synthetic event rather than the DOM KeyboardEvent.
Merged evidence:
-
tusen-ai/naive-ui #8115:
n-dynamic-tagscommitted a tag on the confirm Enter; the PR includes a test that triggerscompositionstartand then Enter. -
onyx-dot-app/onyx #12511:
ListFieldInputadded an item on the confirm Enter. - LibreChat-AI/LibreChat #13996: prompt name, labels and dynamic tag inputs.
- TriliumNext/Trilium #10315: board card and column titles.
- siyuan-note/siyuan #17988: agent session rename input.
- chakra-ui/zag #3198: the color picker channel input committed a partial value on the confirm Enter.
Class 3: Enter selects or searches on unconfirmed text
What the user sees: in an autocomplete, picker or search box, the confirm Enter picks whatever option is highlighted, or fires the search with the reading instead of the converted word.
Minimal repro:
combo.addEventListener("keydown", (e) => {
if (e.key === "Enter") selectHighlighted();
});
Fix pattern: guard first, then select. In component libraries the guard belongs in the shared keydown handler so every consumer gets it.
Merged evidence:
-
payloadcms/payload #17138:
SearchInputsearched on the confirm Enter. -
rsuite/rsuite #4585:
InputPickerselected an item; the PR adds a test thatonChangeis not called for Enter withisComposing: true. -
vuetifyjs/vuetify #22974:
VAutocompletekeydown listener ran during composition.
Related, no PR of mine behind it: search-as-you-type that fires on every input event also queries the unconfirmed reading while the user is still choosing candidates. InputEvent.isComposing is true for those events (MDN), so a common pattern is to skip them and run the query from compositionend instead. Whether that is what your users want is a product choice; I have not tested this pattern across browsers for this post.
Class 4: keyCode 229 where isComposing is false
What the user sees: the fix from Class 1 works in Chrome, but the bug is still there for some Safari users.
Why: the plate PR below documents the Safari plus Japanese IME case where the confirm Enter arrives with keyCode === 229 and isComposing === false. A guard that reads only isComposing lets it through. I did not reproduce this in Safari myself; the claim rests on that PR and on MDN's recommended guard checking both.
Fix pattern: always check both, e.isComposing || e.keyCode === 229. keyCode is deprecated, but this is the one place it still carries information.
Merged evidence:
- udecode/plate #5048: the floating link editor submitted on the Safari confirm Enter.
-
misskey-dev/misskey #17646 (also in Class 1) checks
isComposing,key === "Process"andkeyCode === 229.
Class 5: composition flags that depend on event order
What the user sees: the same as Class 1 or 2, but only in one browser.
Minimal repro:
let composing = false;
input.addEventListener("compositionstart", () => (composing = true));
input.addEventListener("compositionend", () => (composing = false));
input.addEventListener("keydown", (e) => {
if (!composing && e.key === "Enter") submit();
});
Why it breaks: the flag is only correct if the confirm keydown arrives before compositionend. The UI Events spec (section 3.6.5, "Key Events During Composition") says keydown events sent during the composition session must have isComposing set to true. The plate PR in Class 4 shows Safari delivering the confirm Enter with isComposing false, which is consistent with that keydown landing outside the session, after compositionend has already cleared your flag. I have not measured the event order in Safari or any other browser for this post; treat browser-specific ordering as untested.
Fix pattern: read the state off the event itself (e.isComposing || e.keyCode === 229) instead of, or in addition to, a flag you maintain. Where a library keeps an instance flag, as naive-ui #8115 does with its input's composition state, add a test that fires Enter during an active composition.
A bonus class: Escape during composition
Pressing Escape to cancel the IME candidate list also fires keydown with isComposing: true. If a modal or drawer closes on Escape, it closes while the user only meant to cancel the candidates.
-
Tencent/tdesign-vue-next #6756: the drawer closed on the composition-cancel Escape; the fix is
if (e.key === "Escape" && e.isComposing) return;plus a test.
Test your own app in 5 minutes
You do not need to read Japanese. Add the Japanese IME in your OS settings (Windows: Settings, Time & language, Language, add Japanese; macOS: System Settings, Keyboard, Input Sources, add Japanese - Romaji).
- Switch to Japanese input and focus your chat box. Type
toukyou, press Space to see candidates, press Enter once to confirm. Expected:ζ±δΊ¬stays in the box and nothing is sent. Press Enter again; now it sends. - Repeat in every field where Enter does something: tag inputs, list editors, rename fields, inline title editing, search boxes, autocompletes, comboboxes.
- With candidates open, press Escape. Expected: the candidate list closes and your modal or drawer stays open.
- In a search-as-you-type box, type a reading slowly and watch the network tab. Decide whether queries for the unconverted reading are acceptable.
- Grep your code for
key === "Enter",key === 'Enter',keyCode === 13and"Escape", and check each handler hasisComposing || keyCode === 229before it acts. If your handlers set flags incompositionstartandcompositionend, check them against step 1 in every browser you support.
If you can only test on Windows, steps 1 to 4 do not cover the Safari cases in Classes 4 and 5; step 5 does, by reading the code.
This article and the fixes it links were drafted with Claude Code and reviewed by the author.
If you would rather have someone else run this check on your app, I offer it at ime-check.vercel.app.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.