姓名フォームのフリガナ自動入力、ライブラリを1つ入れれば終わる、と思っていませんか。私もそう思っていました。先にたどり着いた形を書いてしまうと、composition イベントで「変換確定前の最後のひらがな」をスナップショットに保存し、compositionend と blur の両方で反映する、という二段構えでした。ATOK 35 + Chrome 148 では、これで安定しました。ただし、ここに行き着くまでに、単純な input イベントだけでは拾えない場面が、いくつも出てきました。
この記事では、姓名フォームのフリガナ自動入力を compositionstart / compositionupdate / compositionend(MDN: CompositionEvent)で自前実装したときの検証メモを、なぜその形に落ち着いたのかという経緯と一緒にまとめます。
日本語入力 UI まわりの記事として、過去に 「あ」で「雨」「赤」を出す日本語サジェストを、HTML と JavaScript だけで作る|IME に頼らない読み検索(基本編) と、その続編の 日本語サジェストの実装版|キーボード操作・blur 競合・WAI-ARIA・API 連携まで直して、ようやく使える検索 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 入力中の状態を追うには、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 で別アプリへ切り替えると、順序が変わりました。
|
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 が来ても、スナップショットは空なので二重入力にはなりません。どちらが先に来ても、消費されるのは1回だけ。このあたりは、実際にログを取らないと気づきにくい部分でした。「blur でも拾っておいたほうが安全そう」という感覚だけではなく、Cmd+Tab で本当に順序が逆転することを確認できたのは大きかったです。
推測変換と、効いたひらがなフィルタ
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 が「山田」に変わりました。ここで、先ほどのひらがなフィルタが効きます。「山田」はひらがなだけではないので、HIRAGANA_RE.test(e.data) が false になり、スナップショットは更新されません。直前に保存していた「やまだ」が残ります。そのため、確定後もフリガナ欄には「やまだ」が入る。正直に言うと、これは最初から狙っていたわけではありません。中間状態のローマ字混じりや漢字を弾くために入れたフィルタが、結果的に推測変換の漢字化も弾いてくれました。
この結果は ATOK + Chrome に依存
これは ATOK 35 + Chrome 148 での実測結果です。別の IME やブラウザでは、同じ順序でイベントが来るとは限りません。たとえば、compositionupdate を経由せずに compositionend.data だけが漢字になる環境があれば、スナップショットが期待どおり残らない可能性があります。
ここで無理に漢字から読みを逆引きしようとすると、別の問題が出ます。「山田」は「やまだ」と読むことが多いですが、漢字の読みは必ず一意ではありません。形態素解析ライブラリを入れれば推測はできますが、姓名フォームのフリガナ補助のためだけに辞書込みの大きなライブラリを積むのは、少し重すぎます。そのため、フリガナ欄は readonly にしないほうがよいと感じました。自動入力はあくまで入力補助で、最後はユーザーが直せるようにしておく。この設計のほうが、実運用では安全です。
ライブ変換は未検証
macOS の日本語入力には、ライブ変換があります。入力中に自動で漢字へ変換される機能です。ライブ変換が有効な環境では、compositionupdate.data がかなり早い段階で漢字になる可能性があります。その場合、今回の「最後のひらがなスナップショットを保持する」方式がどこまで安定するかは、別途確認が必要です。今回の検証環境で使った ATOK 35 では、macOS 標準 IME のようなライブ変換は発生しませんでした。そのため、この記事ではライブ変換 ON の動作までは確認できていません。ここは、この記事の限界として明記しておきます。本番のフォームで使うなら、想定ユーザーが多い環境、特に macOS 標準 IME、Windows + MS-IME、Google 日本語入力では追加検証したほうがよいです。
コピペの限界
姓フィールドに、別の場所からコピーした「山田」を貼り付けた場合も確認しました。この場合、composition イベントは発火しません。フリガナ欄は空のままです。これは不具合というより、仕組み上の限界です。ブラウザには、漢字の「山田」から「やまだ」という読みを正確に作る機能はありません。貼り付けられた文字列がひらがなだけなら、paste イベントでフリガナ欄へコピーすることはできます。ただ、漢字を貼り付けたときに読みを自動生成するには、別の辞書や解析処理が必要になる。今回の実装では、コピペされた漢字からのフリガナ生成は対象外としました。フォームの注意書きや、フリガナ欄を編集可能にしておくことで対応するほうが現実的です。
IME オフの英字入力
IME をオフにした状態で「Smith」のように半角英字を入力した場合も、composition イベントは発火しません。そのため、フリガナ欄には何も入りません。日本語の姓名フォームとして考えるなら、この挙動は許容範囲だと思います。ただし、外国籍のユーザーや英字氏名を想定するフォームでは、そもそも「フリガナ必須」という設計自体を見直したほうがよいかもしれません。少なくとも、英字氏名を入力する可能性があるフォームで、フリガナ欄を強制必須にする場合は、入力例や補足説明を用意したほうが親切です。
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回の入力でフリガナ欄を上書きしたい場合は、+= を = に変更します。姓と名を別々のフィールドにするなら、基本的には上書きでも問題ない場面が多いと思います。実際の案件で使うなら、同じ処理を姓と名で重複して書くより、入力フィールドとフリガナフィールドのペアを引数で渡す関数にしたほうが管理しやすいです。
フリガナ自動入力は、表面だけ見ると簡単そうに見える
姓フィールドの文字を見て、フリガナ欄へ入れるだけ。最初は自分もそのくらいの感覚でした。しかし、実際に IME のイベントログを取ると、変換前のひらがな、変換後の漢字、フォーカス移動、推測変換、blur と compositionend の順序など、いくつもの要素が絡んでいました。特に印象に残ったのは、Cmd+Tab で blur が compositionend より先に来たこと。フォームの実装ではよくある操作なのに、ログを取るまでまったく意識していませんでした。もうひとつは、ひらがなフィルタが ATOK の推測変換を結果的に救っていたこと。最初から完璧に設計できていたわけではなく、動かして、ログを見て、なぜうまくいったのかを後から確認しました。既存ライブラリを使うこと自体は悪くありません。ただ、入力フォームはユーザーが直接触る場所です。とくに姓名や住所のようなフォームでは、少しの違和感が離脱につながる。ライブラリを入れる場合でも、最低限、どのイベントで何を拾っているのか、自分の環境でどう動くのかは確認しておいたほうが安心です。
検証環境と、未確認の環境
この記事のログと挙動確認は、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 まわりのイベントは、ブラウザや日本語入力システムによって発火順序が変わる可能性があります。この記事のコードをそのまま本番投入する場合は、実際に想定するユーザー環境で再検証してください。
参考にした公式ドキュメント
- 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 合成中かどうかを判定するプロパティ。
関連記事
- 「あ」で「雨」「赤」を出す日本語サジェストを、HTML と JavaScript だけで作る|IME に頼らない読み検索(基本編) ― 本記事と同じ composition イベントを使った、日本語サジェスト UI の基礎実装。
- 日本語サジェストの実装版|キーボード操作・blur 競合・WAI-ARIA・API 連携まで直して、ようやく使える検索 UI にした話 ― 上記の続編。本記事と同様、blur まわりの挙動を実測ベースで詰めた話。
- 日本語入力の Enter でフォームが誤送信される問題を直した話|Safari・React・Vue 対応 ― 同じ IME まわりの実装で、Enter キーの誤送信を防いだ記録。
- Contact Form 7 で zipaddr-jp が動かなかった話|郵便番号→住所自動入力で踏んだ id 命名規則の罠 ― 同じ WordPress フォーム実装で、郵便番号→住所自動入力の id 命名規則にハマった記録。
- 日本語フォームの半角カナ・全角英数を input と blur で使い分けて自動変換する話 ― 半角カナや全角英数を、input と blur で役割分担しながら自動変換する話。














コメント