フォーカスとキーボード
マウスを使えない/使わないユーザーは多い(運動機能障害・スクリーンリーダ・キーボード派)。 キーボードだけで全機能にアクセスできることが a11y の必須条件。 フォーカスは「今どこにいるか」を可視化し、思考のアンカーになる。
フォーカス可能な要素
以下の要素はデフォルトでフォーカス可能でありかつ Tab キーの順序にも入る:
<a>withhref<button><input>/<textarea>/<select><iframe><summary>(<details>内)- contenteditable な要素
<div> や <span> はフォーカスできない。
カスタム UI を div で作るとキーボードで触れないのはこれが原因。
tabindex
| 値 | 意味 |
|---|---|
tabindex="0" | Tab 順序に追加(DOM 順) |
tabindex="-1" | Tab 順序に入れないが focus() はできる |
tabindex="1" 以上 | 禁忌。Tab 順序を狂わせる |
tabindex は使わない
tabindex="1" のような正の値を付けると、その要素が Tab 順の先頭に飛び出す。
1つ付けるとサイト全体の順序が壊れていく。必ず DOM 順を物理的に整える。
典型用途
tabindex="0"— div で作ったカスタムボタンに付ける(ただし可能なら<button>に書き換え)tabindex="-1"— モーダルが開いた時に focus を移す要素、エラー要約などプログラム的に focus したい要素
フォーカスインジケータ
outline: none だけ書いて代替を用意しないのが最悪のアンチパターン。
フォーカスが見えなければ、キーボード利用者は今どこにいるか分からない。
:focus-visible を使う
マウスクリック時には outline を出さず、Tab で来た時だけ出す擬似クラス。 現代的なフォーカススタイルの定番。
/* デフォルトの outline は消してよい */
button:focus { outline: none; }
/* キーボードで来た時だけリングを出す */
button:focus-visible {
outline: 2px solid #38bdf8;
outline-offset: 2px;
}
WCAG 2.4.11/2.4.13(Focus Appearance)
- フォーカス時の視覚的な変化が明確(コントラスト 3:1 以上)
- 少なくとも2 CSS ピクセルの太さのラインで囲む
- 他の要素に隠されない
スキップリンク
多くのページはヘッダ・グローバルナビが共通。毎ページ最初から Tab するのは苦痛。 本文の最初に「メインコンテンツへスキップ」リンクを入れるのが定番。
<body>
<a class="skip-link" href="#main">メインコンテンツへスキップ</a>
<header> ... </header>
<main id="main" tabindex="-1"> ... </main>
</body>
.skip-link {
position: absolute;
top: -40px; left: 8px;
background: #000;
color: #fff;
padding: 8px 12px;
z-index: 9999;
}
.skip-link:focus {
top: 8px; /* フォーカス時だけ画面に出す */
}
tabindex="-1" をターゲット要素に付けると、リンクをクリックした後
ジャンプ先にプログラム的にフォーカスが入りやすくなる。
フォーカス順序
Tab 順序はDOM 順。CSS の order や flex-direction: row-reverse で
見た目の順序を変えても、Tab 順は DOM のまま。視覚順と Tab 順がずれると混乱する。
- 視覚的に「右→左」に並ぶなら、DOM も「右→左」に並べる
- 位置をフラットに変えるだけなら CSS でいい
- 順序を入れ替える時は実際の DOM も並べ替える
フォーカストラップ(モーダル)
モーダルダイアログを開いた時、背景の要素に Tab で出てしまわないよう閉じ込める必要がある。
ネイティブ <dialog> を使う
dialog.showModal() で開けば、フォーカストラップ・Esc 閉じ・バックドロップが
全部自動で動く。これで足りるならまずネイティブを使う。
<dialog id="confirm">
<h2>確認</h2>
<form method="dialog">
<button value="cancel">キャンセル</button>
<button value="ok">OK</button>
</form>
</dialog>
<script>
document.getElementById("confirm").showModal()
</script>
自前で実装するなら
- モーダル開いたら、最初のフォーカス可能要素に
focus() - Tab / Shift+Tab がモーダル内をループするように制御
- 背景には
inert属性を付ける(後述) - Esc キーで閉じる
- 閉じたら開いた元のトリガにフォーカスを戻す
自前は実装ミスが多い。ライブラリ(focus-trap, react-focus-lock, Radix Dialog 等)を使うか、
ネイティブの <dialog> を使うのが安全。
inert 属性
要素以下をすべてフォーカス不可・クリック不可・スクリーンリーダ不可にする属性。 モーダル背景や、非アクティブなタブパネルに使う。
<div id="background" inert>
<!-- ここのボタンも input も Tab で来ない -->
</div>
<dialog open> ... </dialog>
aria-hidden は SR からだけ隠すが、inert はキーボード操作も止めるのが大きな違い。
SPA のルート遷移
SPA でクライアントサイドにルート遷移する時、フォーカスを管理する必要がある:
- ページ遷移しても、フォーカスは押したリンクに残ったまま
- 新しいページの内容はスクリーンリーダに通知されない
- 結果、視覚障害者は「クリックしたけど何が起きたか分からない」状態
パターン
// ルート遷移後に呼ぶ
function announcePageChange() {
// メインまたは h1 にフォーカス
const main = document.querySelector("main")
main.setAttribute("tabindex", "-1")
main.focus()
// 一回 focus に使ったら removeAttribute してもいい
}
多くのフレームワーク(Next.js / React Router / Astro)は、ルート遷移時のフォーカス管理を組み込みでサポートしている。
キーボードショートカット
accesskey 属性
accesskey="s" のように1文字を割り当てると、修飾キー + その文字でフォーカスが当たる。
ただしブラウザ・OS で挙動が違ううえ、既存ショートカットと衝突しやすい。使わない方が無難。
独自ショートカット
- 1文字ショートカットを作るなら、トグル可能にする(WCAG 2.1.4)
- Esc / Enter / Space / 矢印キーは特別な意味があるので、独自意味を上書きしない
- ヘルプ画面や
?でショートカット一覧を見られるようにする
キーボード操作の典型パターン
メニュー
- Tab — メニュー全体を1つの単位として通過
- ↓ / ↑ — メニュー項目の移動
- Enter / Space — 選択
- Esc — メニューを閉じてトリガに戻す
- Home / End — 先頭 / 末尾
- 文字キー — その文字で始まる項目へジャンプ
タブ(Tabs)
- Tab — タブリスト → アクティブなパネル へ移動
- ← / → — タブの切替(タブリスト内で)
- Home / End — 最初 / 最後のタブ
- Enter / Space — タブの起動(自動切替なら不要)
ダイアログ
- Esc — 閉じる
- Tab / Shift+Tab — モーダル内ループ
- Enter — デフォルトボタンの実行(OK 等)
詳しくは ARIA Authoring Practices Guide の各パターン。
テスト: キーボードだけで操作する
- マウスから手を離す
- Tab を押し続けて全ての操作要素に到達できるか
- 各要素でフォーカスが見えるか
- Enter / Space で動作するか
- モーダルや menu が開いた後、Esc で閉じてフォーカスが戻るか
- 循環するメニューで「逃げ場」がない事態にならないか
a11y のバグの大半は、マウスから手を離して 30 秒触るだけで見つかる。
チームのレビュー文化に「キーボードだけで通せるか」を入れると劇的に改善する。