姓名フォームのフリガナ自動入力、ライブラリを1つ入れれば終わる、と思っていませんか。私もそう思っていました。先にたどり着いた形を書きます。composition イベントで「変換確定前の最後のひらがな」をスナップショットに保存し、compositionend と blur の両方で反映する、という二段構えでした。ATOK 35 + Chrome 148 では、これで安定しました。ただ、ここに行き着くまでに、単純な input イベントだけでは拾えない場面がいくつも出てきました。この記事は、その自前実装の検証メモを、なぜその形に落ち着いたかと一緒に残すものです。漢字から読みを推測するものではなく、IME で入力中のひらがな状態を拾ってフリガナ欄に反映する実装、という点だけ先に断っておきます。
検証環境:macOS Tahoe 26.4.1 / Google Chrome 148 / ATOK 35.0.3(2026年5月)。IME まわりのイベントは、ブラウザや日本語入力で発火順序が変わる可能性があります。本番投入する場合は、想定するユーザー環境で再検証してください。
input イベントでは、なぜ拾えないのか
最初に書いたのは、素朴なコードでした。姓の input イベントを見て、その値をフリガナ欄へコピーする。「やまだ」と打つ段階ではフリガナ欄にも「やまだ」が入って、動いたように見えます。ところが Space で「山田」に変換して Enter で確定すると、フリガナ欄も「山田」になってしまう。フリガナ欄に欲しいのは確定後の値ではなく、確定前に IME が持っていた「やまだ」のほうです。どこから読みを取るべきか。これがこの記事の出発点でした。
そこで、IME 入力中の状態を追う3つのイベントを使います。compositionstart が入力の始まり、compositionupdate が変換中の文字列の更新、compositionend が確定です。姓に「やまだ」と入れて「山田」に変換し確定するまでをログに出すと、こうなりました。
| 1 2 3 4 5 6 | compositionstart data="" compositionupdate data="や" compositionupdate data="やま" compositionupdate data="やまだ" compositionupdate data="山田" ← Space を押した瞬間 compositionend data="山田" ← Enter で確定 |
最初は compositionend で取ればいいと思っていたのですが、compositionend.data に入っているのは「山田」です。確定後に見に行っても、欲しい「やまだ」は残っていません。フリガナとして使える文字列は、Space で漢字に変わる直前の compositionupdate.data にありました。だから、最後に見つけたひらがなだけの compositionupdate を、スナップショットとして保存しておく方針にしました。
ひらがなだけを、どう拾うか
ひらがなと長音記号だけを許可する正規表現を用意し、compositionupdate のたびに、その値がひらがなだけならスナップショットを更新します。漢字やローマ字混じりが来たら更新しない。確定時に、保存しておいたスナップショットをフリガナ欄へ足します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | const HIRAGANA_RE = /^[\u3040-\u309Fー]*$/; let lastHiraganaSnapshot = ''; surnameInput.addEventListener('compositionstart', () => { lastHiraganaSnapshot = ''; }); surnameInput.addEventListener('compositionupdate', (e) => { if (HIRAGANA_RE.test(e.data)) { lastHiraganaSnapshot = e.data; } }); surnameInput.addEventListener('compositionend', () => { if (lastHiraganaSnapshot) { surnameKanaInput.value += lastHiraganaSnapshot; } lastHiraganaSnapshot = ''; }); |
これで「やまだ」を「山田」に変換すると、姓欄には「山田」、フリガナ欄には「やまだ」が入ります。普通に入力したときは、これで素直に動きました。怖いのは、そこではありません。変換途中でフォーカスが外れたとき、推測変換を使ったとき、コピペしたとき。ここから先が、ログを取らないと見えない領域でした。
Cmd+Tab で、なぜ順序が逆転するのか
変換途中のまま別のフィールドへ移る場合を調べました。姓に「やまだ」と入れて、未変換のまま名フィールドをマウスでクリックすると、先に compositionend が発火し、そのあと change、blur、focusout、名フィールドの focus、と続きます。compositionend.data に「やまだ」が残っているので、フリガナ欄に問題なく反映できます。ところが、同じ未変換の状態で Cmd+Tab で別アプリへ切り替えると、順序が変わりました。
| 1 2 3 4 5 6 | 姓input data="やまだ" isComposing=true 姓keydown key="Meta" 姓change value="やまだ" 姓blur value="やまだ" 姓focusout 姓compositionend data="やまだ" ← blur のあとに来る |
マウスクリックでは compositionend が先でしたが、Cmd+Tab では blur が先に発火し、そのあとで compositionend が来ています。この違いは実装上かなり重要です。compositionend だけに依存していると、環境によっては反映が遅れたり、最悪 compositionend を拾えないまま終わったりする可能性があります。
そこで、blur 時点でも、まだ IME 入力中でひらがなのスナップショットが残っていれば、そこで反映する保険を入れました。compositionend が先に来る場合は、そこでスナップショットを消費し、あとから blur が来ても何もしない。blur が先に来る場合は blur 側で消費し、あとから compositionend が来てもスナップショットは空なので二重入力にならない。どちらが先でも、消費は1回だけです。「blur でも拾っておくと安全そう」という感覚だけでなく、Cmd+Tab で本当に順序が逆転すると確認できたのが大きかったです。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | let isComposing = false; surnameInput.addEventListener('compositionstart', () => { isComposing = true; lastHiraganaSnapshot = ''; }); surnameInput.addEventListener('compositionend', () => { isComposing = false; if (lastHiraganaSnapshot) surnameKanaInput.value += lastHiraganaSnapshot; lastHiraganaSnapshot = ''; }); surnameInput.addEventListener('blur', () => { if (isComposing && lastHiraganaSnapshot) { surnameKanaInput.value += lastHiraganaSnapshot; lastHiraganaSnapshot = ''; isComposing = false; } }); |
推測変換は、なぜ勝手に救われたのか
ATOK には推測変換があります。「やま」と入力している途中で「山田」のような候補が出て、Tab で選べる。このとき compositionupdate.data が「山田」に変わります。ここで、先ほどのひらがなフィルタが効きました。「山田」はひらがなだけではないので HIRAGANA_RE.test が false になり、スナップショットは更新されない。直前に保存していた「やまだ」が残るので、確定後もフリガナ欄には「やまだ」が入ります。正直に言うと、これは狙っていたわけではありません。中間状態のローマ字混じりや漢字を弾くために入れたフィルタが、結果的に推測変換の漢字化も弾いてくれました。
ここで無理に漢字から読みを逆引きしようとすると、別の問題が出ます。漢字の読みは一意ではなく、形態素解析ライブラリを積むのはフリガナ補助には重すぎます。だから、フリガナ欄は readonly にしないほうがよいと感じました。自動入力はあくまで補助で、最後はユーザーが直せるようにしておく。この設計のほうが実運用では安全です。
完成形の最小コード
姓と姓フリガナだけの最小実装は、こうなります。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | <input type="text" id="surname" autocomplete="family-name"> <input type="text" id="surname-kana" autocomplete="off"> <script> const surnameInput = document.getElementById('surname'); const surnameKanaInput = document.getElementById('surname-kana'); const HIRAGANA_RE = /^[\u3040-\u309Fー]*$/; let lastHiraganaSnapshot = ''; let isComposing = false; surnameInput.addEventListener('compositionstart', () => { isComposing = true; lastHiraganaSnapshot = ''; }); surnameInput.addEventListener('compositionupdate', (e) => { if (HIRAGANA_RE.test(e.data)) lastHiraganaSnapshot = e.data; }); surnameInput.addEventListener('compositionend', () => { isComposing = false; if (lastHiraganaSnapshot) surnameKanaInput.value += lastHiraganaSnapshot; lastHiraganaSnapshot = ''; }); surnameInput.addEventListener('blur', () => { if (isComposing && lastHiraganaSnapshot) { surnameKanaInput.value += lastHiraganaSnapshot; lastHiraganaSnapshot = ''; isComposing = false; } }); </script> |
value += としているので、複数回に分けて変換すると追記されます(「やまだ」のあと「たろう」を確定すると「やまだたろう」)。1回で上書きしたいなら += を = にします。実際の案件では、姓と名で同じ処理を重複させず、入力欄とフリガナ欄のペアを引数で渡す関数にしたほうが管理しやすいです。
確認できていないこと
余談として、検証中に気づいた地味な点を1つ。変換途中から Tab で名フィールドへ移ろうとすると、自分では1回のつもりでも Tab を2回押していました。1回目は IME 変換中で、ATOK が推測候補の操作に使う。Enter で確定して isComposing が false になったあと、2回目でようやくフィールドが移ります。「Tab で次のフィールドへ移る」という前提も、IME の状態で変わるわけです。
そして限界も明記しておきます。漢字をコピペした場合は composition イベントが発火せず、フリガナ欄は空のまま(ブラウザに漢字から読みを作る機能はありません)。IME オフの英字入力でも発火しません。さらに、macOS 標準 IME のライブ変換は、compositionupdate がかなり早く漢字になる可能性があり、この「最後のひらがなスナップショット」方式がどこまで安定するかは未確認です。今回の ATOK 35 ではライブ変換は起きませんでした。Safari、Firefox、Windows + MS-IME、Google 日本語入力、iOS / Android、ライブ変換 ON は、まだ確認していません。
表面だけ見ると、姓の文字をフリガナ欄へ入れるだけ。
でも実際にログを取ると、変換前のひらがな、変換後の漢字、フォーカス移動、推測変換、blur と compositionend の順序まで絡んでいました。既存ライブラリを使う場合でも、どのイベントで何を拾っているかは、自分の環境で一度確認しておくほうが安心です。
参考にした公式ドキュメント
- MDN Web Docs: CompositionEvent
- MDN Web Docs: Element: compositionend event
- MDN Web Docs: KeyboardEvent.isComposing
関連記事
- 日本語入力の Enter でフォームが誤送信される問題を直した話|Safari・React・Vue 対応。同じ IME まわりで、Enter キーの誤送信を防いだ記録です。
- 「あ」で「雨」「赤」を出す日本語サジェストを、HTML と JavaScript だけで作る(基本編)。同じ composition イベントを使った、日本語サジェスト UI の基礎です。
- Contact Form 7 で zipaddr-jp が動かなかった話|郵便番号から住所自動入力で踏んだ id 命名規則の罠。同じ WordPress フォーム実装での罠です。
- 日本語フォームの半角カナ・全角英数を input と blur で使い分けて自動変換する話。input と blur で役割分担して自動変換する話です。


コメント