症状
テキスト入力に応じて動くUI ── 文字数カウンタ、送信ボタンの活性化、全角→半角の自動変換 ── が、日本語を入力したときだけ固まる。英数字を打つと正常に動く。Chrome ではどちらも動く。Safari 26 だけで、しかも日本語のときだけ。
たとえば「140文字まで」のカウンタが、英数字では減っていくのに、日本語をIMEで確定した瞬間には更新されない。送信ボタンが disabled のまま解除されない。
原因: IME確定で input が発火しない
原因は、WebKit が日本語IMEの確定時に input イベントを発火しないことがあるという点です。
多くのハンドラは input だけを購読しています。
textarea.addEventListener('input', updateCounter);
英数字の直接入力なら input が飛ぶので動きます。しかし日本語入力は「変換中(composition)→ 確定」という段階を踏みます。この確定のタイミングで input が飛ばないことがある。すると確定した文字がフィールドに入っているのに、ハンドラが呼ばれず、カウンタもボタンも古いまま固まります。
「入力に反応するUI」を作るとき、input だけを頼りにすると、日本語ユーザーのところで静かに壊れます。
対策: 3つのイベントを冪等に購読する
input に加えて、compositionend(IME確定)と change(blur 等の最終フォールバック)も購読します。
function onTextInput(el, handler) {
if (!el || typeof handler !== 'function') return;
el.addEventListener('input', handler);
el.addEventListener('compositionend', handler); // IME 確定を確実に拾う
el.addEventListener('change', handler); // 取りこぼしの最終防波堤
}
compositionend はIMEの変換確定時に必ず飛ぶので、input が来ない確定を補えます。change はフォーカスが外れた時などの最後の砦です。
条件: ハンドラは何度呼ばれても安全に
3つのイベントを購読すると、1回の入力で handler が複数回呼ばれることがあります。input と compositionend が両方飛ぶ場面など。
なので handler は冪等でなければなりません。何度呼んでも同じ結果になる実装にします。
// 良い例: 現在の値から毎回カウントし直す(何度呼んでも同じ)
onTextInput(textarea, function () {
counter.textContent = String(140 - this.value.length);
submitBtn.disabled = this.value.length === 0;
});
「入力があるたびにカウンタを1減らす」のような差分更新は、複数回呼ばれると壊れます。「現在の値から総数を計算する」ように書けば、余分に呼ばれても平気です。全角→半角のサニタイズも、既に半角化済みの文字列を再度サニタイズしても変わらない実装にしておきます。
なぜ input 単独購読が広まっているか
input は「値が変わったら飛ぶ」ものとして直感的で、英数字では実際そう動きます。開発時に英数字でテストして通ってしまうと、日本語IMEの確定で飛ばないケースを踏まないまま出荷されます。日本語で明示的にテストしないと顕在化しないのが、この問題が残りやすい理由です。
対策は難しくありません。テキスト入力に反応するUIを作るときは、最初から input 単独購読を避け、確定と最終フォールバックを併せて購読する。これを共通関数にして、テキスト入力ハンドラは必ずそこを通す運用にしました。
まとめ
- WebKit は日本語IMEの確定時に
inputを発火しないことがある。input単独購読だと、文字数カウンタ・送信ボタン活性・サニタイズが確定時に固まる input+compositionend+changeを併せて購読する。compositionendが確定を確実に拾う- ハンドラは冪等にする。 複数イベントで多重に呼ばれるため、差分更新でなく「現在値から再計算」で書く
- 日本語で明示的にテストする。 英数字だけのテストでは顕在化しない
この記事は、事業・Webサイトの売買プラットフォーム RIKKA M&A を開発する中で得た知見です。RIKKA M&A は GitHub リポジトリを解析する技術デューデリジェンスを備え、Drupal 11 / PHP / LLM で構築・運用しています。

