フォーカスイベントとは何か
フォーカスイベントは「どの要素がキーボード操作の対象になったか/外れたか」を知らせる合図です。代表は focus(入る)、blur(出る)、そしてバブリングする仲間の focusin(入る)/focusout(出る)。ここが重要です:focus/blur はバブリングしないので“親でまとめて受ける”には向きません。親で一括処理したいなら focusin/focusout を使います。
基本の使い方(focus / blur と focusin / focusout)
個別要素で扱う(focus / blur)
<input id="name" placeholder="名前">
<script>
const name = document.getElementById("name");
name.addEventListener("focus", () => {
console.log("名前にフォーカスが入りました");
name.classList.add("highlight");
});
name.addEventListener("blur", () => {
console.log("フォーカスが外れました");
name.classList.remove("highlight");
});
</script>
HTMLここが重要です:focus/blur は“その要素だけ”の入出を見たいときに使います。スタイルの切り替えや軽い検証に向いています。
親でまとめて扱う(focusin / focusout)
<form id="f">
<input name="email" type="email" placeholder="メール">
<input name="age" type="number" placeholder="年齢">
</form>
<script>
f.addEventListener("focusin", (e) => {
e.target.classList.add("focus");
});
f.addEventListener("focusout", (e) => {
e.target.classList.remove("focus");
});
</script>
HTMLここが重要です:focusin/focusout はバブリングするため、親に1つだけリスナーを付けて“どの子に入ったか”を e.target で判定できます。動的に追加した入力にも自動対応でき、管理が楽になります。
キーボード操作と tabindex(どこで受けるかを設計する)
非入力要素をフォーカス可能にする
<div id="pane" tabindex="0" style="border:1px solid #ccc">ここで矢印キーを受ける</div>
<script>
pane.addEventListener("focus", () => pane.classList.add("active"));
pane.addEventListener("blur", () => pane.classList.remove("active"));
pane.addEventListener("keydown", (e) => {
if (e.key.startsWith("Arrow")) console.log("矢印:", e.key);
});
</script>
HTMLここが重要です:tabindex=”0″ を付けると、div なども“タブ移動の対象”になります。キーボードで操作したい領域は、フォーカス可能にしてそこでイベントを受ける設計にします。
タブ順に入れないがプログラムでフォーカスしたい(tabindex=-1)
<button id="open">開く</button>
<div id="dialog" tabindex="-1" hidden>ダイアログ</div>
<script>
open.addEventListener("click", () => {
dialog.hidden = false;
dialog.focus(); // ユーザーのタブ順に混ざらず、APIでだけフォーカス
});
</script>
HTMLここが重要です:tabindex=”-1″ は“タブ移動に参加しないが focus() はできる”。モーダルの初期フォーカスや、エラーメッセージへ一時的にフォーカスを当てるときに便利です。
検証と状態同期(blurで確定、focusで準備)
blur で確定検証、focus で補助表示
<input id="email" type="email" placeholder="example@example.com" required>
<div id="hint" hidden>会社のメールを推奨</div>
<script>
email.addEventListener("focus", () => { hint.hidden = false; });
email.addEventListener("blur", () => {
hint.hidden = true;
if (!email.checkValidity()) {
email.classList.add("error");
} else {
email.classList.remove("error");
}
});
</script>
HTMLここが重要です:入力中は“補助表示”、フォーカスが外れたタイミングで“厳密な検証”。ライブ中に赤字連発はストレスなので、確定時の優しい設計にします。
フォーカス移動でボタンの有効/無効を同期
<form id="f">
<input id="name" required>
<button id="next" disabled>次へ</button>
</form>
<script>
const sync = () => { next.disabled = !f.checkValidity(); };
f.addEventListener("focusin", sync);
f.addEventListener("focusout", sync);
f.addEventListener("input", sync); // 値の変化にも対応
</script>
HTMLここが重要です:focus と input を組み合わせ、“その瞬間の妥当性”でボタン状態を常に同期します。単一ロジックで制御するとズレが起きません。
モーダルとフォーカストラップ(閉じるまで内側に留める)
モーダル内にフォーカスを閉じ込める
<div id="backdrop" hidden>
<div id="modal" role="dialog" aria-modal="true">
<button id="close">閉じる</button>
<a href="#">リンク</a>
<input placeholder="入力">
</div>
</div>
<script>
function openModal() {
backdrop.hidden = false;
modal.focus(); // 初期フォーカス
document.addEventListener("focusin", trap, { capture: true });
}
function closeModal() {
backdrop.hidden = true;
document.removeEventListener("focusin", trap, { capture: true });
}
function trap(e) {
if (!backdrop.hidden && !modal.contains(e.target)) {
e.stopPropagation(); // 外へ出ようとするフォーカスを遮断
modal.focus();
}
}
close.addEventListener("click", closeModal);
</script>
HTMLここが重要です:モーダルが開いている間は“外側へフォーカスが逃げない”設計が必要。focusin(キャプチャ)で外への移動を見張り、内側へ戻します。Esc キーで閉じるなどキーボード操作も合わせると直感的です。
e.relatedTarget で“移動元/移動先”を掴む
どこから来て、どこへ行ったか
<input id="a"><input id="b">
<script>
a.addEventListener("blur", (e) => {
console.log("a から出た。移動先:", e.relatedTarget); // 次にフォーカスされた要素
});
b.addEventListener("focus", (e) => {
console.log("b に入った。移動元:", e.relatedTarget); // 直前にフォーカスされていた要素
});
</script>
HTMLここが重要です:FocusEvent の relatedTarget で“前後の相手”がわかります。連携 UI(片方からもう片方へ値移行など)を実装するときに便利です。環境によって null になる場合がある点は考慮しましょう。
よくある落とし穴と回避策
focus/blur を親で受けようとして動かない
focus/blur はバブリングしません。ここが重要です:親一括の処理は focusin/focusout を使う。委譲の基本は“バブルで受ける”ことです。
preventDefault でフォーカス移動を止めようとする
フォーカスイベントはキャンセル不可です。ここが重要です:止める設計ではなく“行き先を正しく誘導する”設計にする(tabindex、focus() の活用、フォーカストラップ)。
キーボードフォーカスを奪う UI
クリック中心の設計でフォーカスを無視すると、キーボードユーザーが操作できません。ここが重要です:button/a/input など“適切な要素”を使い、初期フォーカスやタブ順(tabindex)を意識します。
エラー表示のタイミングが早すぎる
入力途中に blur なしで厳密検証を連発すると体験が悪化。ここが重要です:input 中は穏やかな補助、blur で確定検証の二段構えにする。
実践例(フィールドハイライト、ステップフォーム、ヘルプ提示)
今いるフィールドをハイライト
<form id="f">
<input placeholder="氏名">
<input placeholder="メール">
</form>
<script>
f.addEventListener("focusin", (e) => e.target.classList.add("active"));
f.addEventListener("focusout", (e) => e.target.classList.remove("active"));
</script>
HTMLステップフォーム:blur でステップ進行
<form id="step">
<input name="email" type="email" required placeholder="メール">
<div id="next" hidden>次のステップへ進めます</div>
</form>
<script>
step.addEventListener("focusout", () => {
if (step.checkValidity()) next.hidden = false;
});
</script>
HTMLフォーカス時に補助を表示、外れたら隠す
<label>
パスワード
<input id="pw" type="password">
<span id="rule" hidden>8文字以上・数字と英字を含む</span>
</label>
<script>
pw.addEventListener("focus", () => rule.hidden = false);
pw.addEventListener("blur", () => rule.hidden = true);
</script>
HTMLまとめ
フォーカスイベントは「どの要素が操作対象か」を教えてくれる基盤です。個別は focus/blur、親でまとめるなら focusin/focusout。tabindex で“どこに届くか”を設計し、blur で確定検証、focus で補助表示の二段構えにする。モーダルではフォーカストラップで内側に留め、relatedTarget で前後関係を活用する。キャンセル不可であることを前提に、“正しく誘導する”設計へ徹すれば、初心者でも気持ちよく操作できるキーボードフレンドリーな UI を作れます。
