フォーカスとキーボード

マウスを使えない/使わないユーザーは多い(運動機能障害・スクリーンリーダ・キーボード派)。 キーボードだけで全機能にアクセスできることが a11y の必須条件。 フォーカスは「今どこにいるか」を可視化し、思考のアンカーになる。

フォーカス可能な要素

以下の要素はデフォルトでフォーカス可能でありかつ Tab キーの順序にも入る:

<div><span> はフォーカスできない。 カスタム UI を div で作るとキーボードで触れないのはこれが原因。

tabindex

意味
tabindex="0"Tab 順序に追加(DOM 順)
tabindex="-1"Tab 順序に入れないが focus() はできる
tabindex="1" 以上禁忌。Tab 順序を狂わせる
正の tabindex は使わない

tabindex="1" のような正の値を付けると、その要素が Tab 順の先頭に飛び出す。 1つ付けるとサイト全体の順序が壊れていく。必ず DOM 順を物理的に整える

典型用途

フォーカスインジケータ

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)

スキップリンク

多くのページはヘッダ・グローバルナビが共通。毎ページ最初から 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 の orderflex-direction: row-reverse で 見た目の順序を変えても、Tab 順は DOM のまま。視覚順と Tab 順がずれると混乱する。

フォーカストラップ(モーダル)

モーダルダイアログを開いた時、背景の要素に 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-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 で挙動が違ううえ、既存ショートカットと衝突しやすい。使わない方が無難

独自ショートカット

キーボード操作の典型パターン

メニュー

タブ(Tabs)

ダイアログ

詳しくは ARIA Authoring Practices Guide の各パターン。

テスト: キーボードだけで操作する

  1. マウスから手を離す
  2. Tab を押し続けて全ての操作要素に到達できるか
  3. 各要素でフォーカスが見えるか
  4. Enter / Space で動作するか
  5. モーダルや menu が開いた後、Esc で閉じてフォーカスが戻るか
  6. 循環するメニューで「逃げ場」がない事態にならないか
「キーボードでテスト」は最強の早期発見

a11y のバグの大半は、マウスから手を離して 30 秒触るだけで見つかる。
チームのレビュー文化に「キーボードだけで通せるか」を入れると劇的に改善する。