姓名フォームの「フリガナ自動入力」は、見た目以上にやっかいでした。最初は、既存のライブラリを入れればすぐ終わるだろうと思っていました。ところが、IMEの変換中に何が起きているのかをログで追いかけてみると、単純な input イベントだけでは拾えない場面がいくつも出てきました。
この記事では、姓名フォームのフリガナ自動入力を compositionstart / compositionupdate / compositionend(MDN: CompositionEvent) で自前実装したときの検証メモをまとめます。
日本語入力 UI まわりの記事として、過去に 「あ」→「雨」を自力で実装する|IMEに頼らない日本語サジェストの作り方 と、その続編の 日本語サジェストの実装版|キーボード操作とblur競合まで直して、ようやく使える検索UIにした話 も書いています。本記事と地続きの話題なので、合わせて読んでいただくと、composition イベントまわりの全体像が見えやすいかもしれません。
この記事のスコープ
ここで紹介するコードは万能な読み仮名変換ではありません。漢字から読みを推測するものではなく、IMEで入力中の「ひらがな状態」を拾って、フリガナ欄に反映するための実装です。
既存ライブラリをそのまま使うのが少し怖かった
姓名フォームにフリガナを自動入力するUIは、日本向けのフォームではよく見かけます。お問い合わせフォーム、会員登録フォーム、予約フォームなど、実装する機会も多いです。
最初に候補にしたのは、jquery.autoKana.js でした。使い方も短く、昔から知られているライブラリです。試すだけなら、10分もあれば動かせます。
ただ、実際に導入する前にGitHubを見て、少し手が止まりました。最終更新がかなり古く、jQuery依存の実装です。もちろん、古いライブラリがすべて悪いわけではありません。今でも安定して動くコードはあります。
それでも、いま新しくフォームを実装するなら、IMEまわりの挙動を一度自分で確かめておきたいと思いました。
特に気になったのは、macOSのライブ変換やATOKの推測変換のように、ユーザーがEnterで確定する前に候補が変化する入力環境です。こういう場面で、フリガナ欄がどう動くのか。あとから「変換途中にフリガナが消える」「漢字が入る」と言われたときに、自分で説明できないのは困ります。
そこで今回は、既存ライブラリを貼って終わりにせず、compositionイベントを使って最小構成から組んでみることにしました。
inputイベントだけでは、変換後の漢字を拾ってしまう
最初に書いたのは、かなり素朴なコードでした。
|
1 2 3 |
surnameInput.addEventListener('input', (e) => { surnameKanaInput.value = e.target.value; }); |
姓フィールドに「やまだ」と入力すると、フリガナ欄にも「やまだ」と入ります。この時点では、動いたように見えます。
ところが、そのままSpaceキーで「山田」に変換し、Enterで確定すると、フリガナ欄も「山田」になってしまいました。
フリガナ欄に欲しいのは、確定後の値ではありません。確定前にIMEが持っていた「やまだ」のほうです。
input イベントは、フォームの値が変わったことを知るには便利です。ただ、IME変換中の「まだ確定していないひらがな」を安全に拾う用途には向いていませんでした。
compositionイベントでIME入力の途中経過を見る
IME入力中の状態を追うには、次の3つのイベントを使います。
compositionstart:IME入力が始まったときcompositionupdate:変換中の文字列が更新されたときcompositionend:IME入力が確定したとき
実際に、姓フィールドへ「やまだ」と入力し、Spaceキーで「山田」に変換してからEnterで確定するまでをログに出しました。検証環境は、ATOK 35 + Chrome 148 です。
|
1 2 3 4 5 6 7 8 9 |
compositionstart data="" compositionupdate data="y" compositionupdate data="や" compositionupdate data="やm" compositionupdate data="やま" compositionupdate data="やまd" compositionupdate data="やまだ" compositionupdate data="山田" ← Spaceを押した瞬間 compositionend data="山田" ← Enterで確定 |
ログを見て、少し考えが変わりました。
最初は、compositionend でフリガナを取ればいいと思っていました。しかし、compositionend.data に入っているのは「山田」です。つまり、確定後に見に行っても、欲しかった「やまだ」はもう残っていません。
フリガナとして使える文字列は、Spaceキーで漢字に変換される直前の compositionupdate.data にありました。
そのため、今回の実装では「最後に見つけた、ひらがなだけのcompositionupdate」をスナップショットとして保存しておく方針にしました。
ひらがなのスナップショットを保存する
まず、ひらがなだけを許可する正規表現を用意します。
|
1 |
const HIRAGANA_RE = /^[\u3040-\u309Fー]*$/; |
通常のひらがなに加えて、長音記号も許可しています。名前入力では「りょう」「しょうたろう」のような入力が多いので、この範囲で十分かどうかは案件ごとに確認が必要です。
最小構成は、次のようになります。
|
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 = ''; }); |
このコードで、姓フィールドに「やまだ」と入力して「山田」に変換すると、姓フィールドには「山田」、フリガナ欄には「やまだ」が入ります。
ここまでは、かなり素直に動きました。ただ、フォームの実装で怖いのは、こういう「普通に入力したとき」ではありません。変換途中でフォーカスが外れたとき、推測変換を使ったとき、コピペしたときにどうなるかです。
変換途中にフォーカスが外れたときの挙動
次に、変換途中のまま別のフィールドへ移動した場合を調べました。検証用のHTMLを組んで、composition / focus / blur / input / keydown / keyup の各イベントを連番付きでログに出しています。
最初は「やまだ」と入力した途中でTabキーを押せば、名フィールドへ移動できるだろうと思っていました。ところが、ATOK 35 + Chrome 148 では、この想定どおりには動きませんでした。
変換中にTabキーを押すと、ATOK側が推測候補の操作としてTabを使ってしまいます。フォームのフォーカスは姓フィールドに残ったままで、名フィールドへは移動しません。
そこで、実際に起きるフォーカス外れのパターンとして、マウスクリックとCmd+Tabを試しました。
▸マウスで別フィールドをクリックした場合
姓に「やまだ」と入力し、未変換のまま名フィールドをクリックしたときのログです。
|
1 2 3 4 5 6 7 |
#027 5568.9ms 姓compositionupdate data="やまだ" #029 5668.5ms 姓keyup key="a" #030 6650.7ms 姓compositionend data="やまだ" value="やまだ" #031 6658.3ms 姓change value="やまだ" #032 6658.9ms 姓blur value="やまだ" #033 6659.1ms 姓focusout #034 6659.2ms 名focus |
この場合は、先に compositionend が発火しました。そのあとに change、blur、focusout、名フィールドの focus が続きます。
compositionend.data には「やまだ」が残っているので、フリガナ欄にも問題なく反映できます。
▸Cmd+Tabで別アプリへ移動した場合
次に、姓に「やまだ」と入力したまま、Cmd+Tabで別アプリへ切り替えました。
|
1 2 3 4 5 6 |
#028 5151.1ms 姓input data="やまだ" isComposing=true #030 10244.1ms 姓keydown key="Meta" code="MetaLeft" #031 14919.6ms 姓change value="やまだ" #032 14919.9ms 姓blur value="やまだ" #033 14920.0ms 姓focusout #034 14920.1ms 姓compositionend data="やまだ" value="やまだ" |
発火順序の逆転
マウスクリックでは compositionend が先でしたが、Cmd+Tabでは blur が先に発火しています。そのあとで compositionend が来ています。
この順序の違いは、実装上かなり重要です。compositionend だけに依存していると、環境によってはフリガナの反映タイミングが遅れたり、最悪の場合は compositionend を拾えないまま終わったりする可能性があります。
そこで、blur 時点でも、まだIME入力中でひらがなのスナップショットが残っていれば、そこでフリガナ欄へ反映する保険を入れました。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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; } }); |
マウスクリックのように compositionend が先に来る場合は、そこでスナップショットを消費します。あとから blur が来ても、もう何もしません。
逆に、Cmd+Tabのように blur が先に来る場合は、blur 側でスナップショットを消費します。そのあとで compositionend が来ても、スナップショットは空なので二重入力にはなりません。
このあたりは、実際にログを取らないと気づきにくい部分でした。「blurでも拾っておいたほうが安全そう」という感覚だけではなく、Cmd+Tabで本当に順序が逆転することを確認できたのは大きかったです。
ATOKの推測変換では、ひらがなフィルタが効いた
ATOKには推測変換があります。「やま」と入力している途中でも、「山田」のような候補が出てくることがあります。
この推測候補をTabキーで選んだとき、compositionイベントがどう流れるのかも確認しました。
|
1 2 3 4 5 |
#027 4271.0ms 姓compositionupdate data="やまだ" #030 9332.6ms 姓keydown key="Tab" #031 9332.9ms 姓compositionupdate data="山田" ← Tabで推測候補が選択 #034 12765.7ms 姓keydown key="Enter" #037 12767.7ms 姓compositionend data="山田" |
Tabキーで推測候補を選んだ瞬間、compositionupdate.data が「山田」に変わりました。
ここで、先ほどのひらがなフィルタが効きます。
|
1 2 3 |
if (HIRAGANA_RE.test(e.data)) { lastHiraganaSnapshot = e.data; } |
「山田」はひらがなだけではないので、スナップショットは更新されません。直前に保存していた「やまだ」が残ります。そのため、確定後もフリガナ欄には「やまだ」が入ります。
正直に言うと、これは最初から狙っていたわけではありません。中間状態のローマ字混じりや漢字を弾くために入れたフィルタが、結果的に推測変換の漢字化も弾いてくれました。
この結果はATOK + Chromeに依存
これはATOK 35 + Chrome 148での実測結果です。別のIMEやブラウザでは、同じ順序でイベントが来るとは限りません。たとえば、compositionupdate を経由せずに compositionend.data だけが漢字になる環境があれば、スナップショットが期待どおり残らない可能性があります。
ここで無理に漢字から読みを逆引きしようとすると、別の問題が出ます。「山田」は「やまだ」と読むことが多いですが、漢字の読みは必ず一意ではありません。形態素解析ライブラリを入れれば推測はできますが、姓名フォームのフリガナ補助のためだけに辞書込みの大きなライブラリを積むのは、少し重すぎます。
そのため、フリガナ欄は readonly にしないほうがよいと感じました。自動入力はあくまで入力補助で、最後はユーザーが直せるようにしておく。この設計のほうが、実運用では安全です。
macOSのライブ変換は、今回の環境では未検証
macOSの日本語入力には、ライブ変換があります。入力中に自動で漢字へ変換される機能です。
ライブ変換が有効な環境では、compositionupdate.data がかなり早い段階で漢字になる可能性があります。その場合、今回の「最後のひらがなスナップショットを保持する」方式がどこまで安定するかは、別途確認が必要です。
今回の検証環境で使ったATOK 35では、macOS標準IMEのようなライブ変換は発生しませんでした。そのため、この記事ではライブ変換ONの動作までは確認できていません。
ここは、この記事の限界として明記しておきます。本番のフォームで使うなら、想定ユーザーが多い環境、特にmacOS標準IME、Windows + MS-IME、Google日本語入力では追加検証したほうがよいです。
コピペではフリガナを作れない
姓フィールドに、別の場所からコピーした「山田」を貼り付けた場合も確認しました。
この場合、compositionイベントは発火しません。フリガナ欄は空のままです。
これは不具合というより、仕組み上の限界です。ブラウザには、漢字の「山田」から「やまだ」という読みを正確に作る機能はありません。
貼り付けられた文字列がひらがなだけなら、paste イベントでフリガナ欄へコピーすることはできます。ただ、漢字を貼り付けたときに読みを自動生成するには、別の辞書や解析処理が必要になります。
今回の実装では、コピペされた漢字からのフリガナ生成は対象外としました。フォームの注意書きや、フリガナ欄を編集可能にしておくことで対応するほうが現実的です。
IMEをオフにして英字を入力した場合
IMEをオフにした状態で「Smith」のように半角英字を入力した場合も、compositionイベントは発火しません。
そのため、フリガナ欄には何も入りません。
日本語の姓名フォームとして考えるなら、この挙動は許容範囲だと思います。ただし、外国籍のユーザーや英字氏名を想定するフォームでは、そもそも「フリガナ必須」という設計自体を見直したほうがよいかもしれません。
少なくとも、英字氏名を入力する可能性があるフォームで、フリガナ欄を強制必須にする場合は、入力例や補足説明を用意したほうが親切です。
ATOK + Chromeでは、Tab移動が2回押しになることがあった
検証中に、もうひとつ地味な発見がありました。
変換中の状態から名フィールドへ移動しようとすると、自分では自然にTabで移動しているつもりでも、実際にはTabを2回押していました。
|
1 2 3 4 5 6 7 8 |
#030 9332.6ms 姓keydown key="Tab" isComposing=true ← 1回目 #031 9332.9ms 姓compositionupdate data="山田" #034 12765.7ms 姓keydown key="Enter" #037 12767.7ms 姓compositionend data="山田" #039 13849.1ms 姓keydown key="Tab" isComposing=false ← 2回目 #040 13849.9ms 姓change value="山田" #041 13850.2ms 姓blur #043 13850.6ms 名focus |
1回目のTabは、IME変換中だったため、ATOK側が推測候補の操作に使っていました。フォームのフォーカス移動には使われていません。
Enterで変換を確定して isComposing=false になったあと、2回目のTabでようやく名フィールドへ移動しています。
「ユーザーはTabで次のフィールドへ移動する」という前提は、IMEの状態によって変わります。普段何気なく使っている入力操作でも、ログを取ると違う見え方になります。
完成形の最小コード
ここまでの検証を踏まえて、姓フィールドと姓フリガナフィールドだけの最小実装をまとめると、次のようになります。
|
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
<form> <label> 姓 <input type="text" id="surname" autocomplete="family-name"> </label> <label> 姓フリガナ <input type="text" id="surname-kana" autocomplete="off"> </label> </form> <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> |
surnameKanaInput.value += lastHiraganaSnapshot; としているので、複数回に分けて変換した場合はフリガナが追記されます。
たとえば、「やまだ」を確定したあとに続けて「たろう」を確定すると、フリガナ欄は「やまだたろう」になります。
1回の入力でフリガナ欄を上書きしたい場合は、+= を = に変更します。姓と名を別々のフィールドにするなら、基本的には上書きでも問題ない場面が多いと思います。
実際の案件で使うなら、同じ処理を姓と名で重複して書くより、入力フィールドとフリガナフィールドのペアを引数で渡す関数にしたほうが管理しやすいです。
検証環境
この記事のログと挙動確認は、次の環境で行いました。
- macOS Tahoe 26.4.1
- Google Chrome 148
- ATOK 35.0.3
次の環境では、まだ確認していません。
- Safari
- Firefox
- Edge
- Windows + MS-IME
- Google日本語入力
- iOS / Android
- macOS標準IMEのライブ変換ON
IMEまわりのイベントは、ブラウザや日本語入力システムによって発火順序が変わる可能性があります。この記事のコードをそのまま本番投入する場合は、実際に想定するユーザー環境で再検証してください。
今回の実装で分かったこと
フリガナ自動入力は、表面だけ見ると簡単そうに見えます。
姓フィールドの文字を見て、フリガナ欄へ入れるだけ。最初は自分もそのくらいの感覚でした。
しかし、実際にIMEのイベントログを取ると、変換前のひらがな、変換後の漢字、フォーカス移動、推測変換、blurとcompositionendの順序など、いくつもの要素が絡んでいることが分かりました。
特に印象に残ったのは、Cmd+Tabで blur が compositionend より先に来たことです。フォームの実装ではよくある操作なのに、ログを取るまでまったく意識していませんでした。
もうひとつは、ひらがなフィルタがATOKの推測変換を結果的に救っていたことです。最初から完璧に設計できていたわけではありません。動かして、ログを見て、なぜうまくいったのかを後から確認しました。
既存ライブラリを使うこと自体は悪くありません。ただ、入力フォームはユーザーが直接触る場所です。とくに姓名や住所のようなフォームでは、少しの違和感が離脱につながります。
ライブラリを入れる場合でも、最低限、どのイベントで何を拾っているのか、自分の環境でどう動くのかは確認しておいたほうが安心です。
今回の実装は、漢字から読みを生成する万能な仕組みではありません。コピペやライブ変換など、まだ確認できていない課題も残っています。それでも、IME入力中のひらがなを拾ってフリガナ欄に反映する、という範囲では、挙動の理由を説明できる実装になりました。
フォームの小さな便利機能でも、ログを取って確かめると意外と奥が深いです。今回の検証は、次に姓名フォームを実装するときの判断材料として、かなり役に立ちそうです。
参考にした公式ドキュメント
- MDN Web Docs: CompositionEvent ― composition 系イベントの基底インターフェース仕様。
- MDN Web Docs: Element: compositionstart event ― IME 入力開始時に発火するイベントの仕様。
- MDN Web Docs: Element: compositionend event ― IME 確定時に発火するイベントの仕様。
- MDN Web Docs: KeyboardEvent.isComposing ― IME 合成中かどうかを判定するプロパティ。
関連記事
-
- 「あ」→「雨」を自力で実装する|IMEに頼らない日本語サジェストの作り方 ― 本記事と同じ composition イベントを使った、日本語サジェスト UI の基礎実装。
- 日本語サジェストの実装版|キーボード操作とblur競合まで直して、ようやく使える検索UIにした話 ― 上記の続編。本記事と同様、blur まわりの挙動を実測ベースで詰めた話。
- 日本語入力のEnterでフォームが誤送信される問題を直した話|Safari・React・Vue対応 ― 同じ IME まわりの実装で、Enter キーの誤送信を防いだ記録。
- Contact Form 7でzipaddr-jpが動かなかった話|郵便番号→住所自動入力で踏んだid命名規則の罠
- 日本語フォームの半角カナ・全角英数をinputとblurで使い分けて自動変換する話














コメント