Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 6 min read

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:

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:

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:

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:

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).

  1. 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.
  2. Repeat in every field where Enter does something: tag inputs, list editors, rename fields, inline title editing, search boxes, autocompletes, comboboxes.
  3. With candidates open, press Escape. Expected: the candidate list closes and your modal or drawer stays open.
  4. 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.
  5. Grep your code for key === "Enter", key === 'Enter', keyCode === 13 and "Escape", and check each handler has isComposing || keyCode === 229 before it acts. If your handlers set flags in compositionstart and compositionend, 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.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.