フォーカス制御の基本 — el.focus()
フォームやボタンに「今ここを操作できます」と意識を集めるのがフォーカスです。el.focus() は要素にプログラムからフォーカスを当てる最小の一手。入力欄のカーソル移動、モーダルを開いた直後の初期フォーカスなどに使います。
基本の使い方
<input id="name" placeholder="お名前">
<button id="go">フォーカス</button>
<script>
const input = document.getElementById("name");
document.getElementById("go").addEventListener("click", () => {
input.focus(); // 入力欄にカーソルが移動
});
</script>
HTML- 役割: その要素にキーボード入力やショートカットを受け付ける状態を作る。
- 対象: ふつうはフォーム要素(input、textarea、select)、ボタン、リンクなどがフォーカス可能。
よく使うテンプレート集
ラベル: モーダルを開いたら最初の入力にフォーカス
function openModal() {
const modal = document.getElementById("modal");
modal.classList.add("show");
const firstField = modal.querySelector("input, button, [tabindex]:not([tabindex='-1'])");
firstField?.focus({ preventScroll: true }); // スクロールさせずにフォーカス
}
JavaScriptラベル: エラー箇所にジャンプ
function showError(el, msg) {
const hint = el.nextElementSibling;
hint.textContent = msg;
el.focus(); // すぐ修正しやすい
}
JavaScriptラベル: 入力欄を作った直後にフォーカス(タイミング調整)
const list = document.getElementById("list");
function addEditableItem() {
const li = document.createElement("li");
li.innerHTML = `<input type="text" class="edit">`;
list.appendChild(li);
requestAnimationFrame(() => {
li.querySelector(".edit").focus(); // DOMに追加されたフレーム後に確実にフォーカス
});
}
JavaScriptラベル: キャレット(カーソル)を末尾に置く
function focusEnd(input) {
input.focus();
const v = input.value;
input.setSelectionRange(v.length, v.length);
}
JavaScriptフォーカス可能にする設定(tabindex)
<div id="card" tabindex="0">カード(フォーカス可能)</div>
<script>
const card = document.getElementById("card");
card.addEventListener("focus", () => card.classList.add("focused"));
card.addEventListener("blur", () => card.classList.remove("focused"));
</script>
HTML- tabindex=0: その要素を「標準のタブ移動順」に含める。
- tabindex>0: 独自の順序を強制できるが複雑化しやすいので避けるのが無難。
- tabindex=-1: フォーカスはできる(
el.focus()可)が、Tabキーでは移動しない。モーダルの閉じるボタンなどに便利。
イベントとフォーカスの流れ
- focus / blur: 対象要素で発火。バブリングしない。
- focusin / focusout: 親にも届く(バブリングする)。フォーム全体で一括管理に便利。
const form = document.querySelector("form");
form.addEventListener("focusin", (e) => {
e.target.classList.add("has-focus");
});
form.addEventListener("focusout", (e) => {
e.target.classList.remove("has-focus");
});
JavaScript- ラベル: 1箇所のハンドラでフォーム内の全フィールドの入/抜けを監視できる。
実務でのコツ
- 表示状態との整合: 非表示(
display: none)やdisabledの要素にはフォーカスできない。表示してからフォーカスする。 - preventScroll を活用:
el.focus({ preventScroll: true })で意図せぬスクロールを防ぐ。 - タイミング: DOM追加直後はフォーカスが効かないことがある。
requestAnimationFrameやsetTimeout(0)で次のタイミングにずらす。 - アウトラインは残す: CSSで
outline: noneを安易に消すとキーボードユーザーが迷う。代替のフォーカススタイルを必ず用意する。
ありがちなハマりポイントと対策
- フォーカスできない(disabled/hidden):
- 対策: 有効化して表示してから
focus()。visibility: hiddenはフォーカス可能だが見えないので注意。
- 対策: 有効化して表示してから
- スクロールが勝手に動く:
- 対策:
focus({ preventScroll: true })を使う。
- 対策:
- Tab移動の順番が混乱:
- 対策:
tabindex > 0は原則使わず、DOM順に沿った設計に。
- 対策:
- モーダルの外へフォーカスが逃げる:
- 対策: 初期フォーカス+フォーカストラップ(Tabでモーダル内を循環させる)。必要ならキーハンドリングで対応。
練習問題(手を動かして覚える)
<div id="app">
<button id="open">モーダルを開く</button>
<div id="modal" class="modal" aria-hidden="true">
<input id="email" type="email" placeholder="Email">
<button id="submit">送信</button>
<button id="close">閉じる</button>
</div>
</div>
<style>
.modal { display: none; gap: 8px; }
.modal.show { display: flex; }
:focus { outline: 2px solid #4c9aff; }
</style>
<script>
const modal = document.getElementById("modal");
const openBtn = document.getElementById("open");
const closeBtn = document.getElementById("close");
const email = document.getElementById("email");
openBtn.addEventListener("click", () => {
modal.classList.add("show");
modal.setAttribute("aria-hidden", "false");
// スクロールさせずに初期フォーカス
email.focus({ preventScroll: true });
});
closeBtn.addEventListener("click", () => {
modal.classList.remove("show");
modal.setAttribute("aria-hidden", "true");
openBtn.focus(); // 元のトリガーへ戻す
});
// Enterで送信ボタンへ移動してみる
email.addEventListener("keydown", (e) => {
if (e.key === "Enter") document.getElementById("submit").focus();
});
</script>
HTML直感的な指針
- 「見える・有効」な要素に、適切なタイミングで
el.focus()を当てる。 - スクロール抑制やフォーカス順は小さく整える。
tabindex=0/-1を理解して最小限で。 - 見た目のアウトラインは残して、キーボード操作の手触りを大事にする。
