キーボード操作とフォーカス管理の実装ガイド|WCAG 2.2、タブ、モーダルの要点

brown wooden destination arrow guide
Photo by Pixabay on Pexels.com

キーボードで操作できることは、Webアクセシビリティの基本です。

フォーカスとは、次のキー入力を受け取る要素を示す状態です。
フォーカスの位置が見えない、移動順が画面の流れと合わない、モーダルを閉じた後に元の場所へ戻れないといった問題は、操作の中断や誤操作につながります。

この記事では、通常のリンクやボタンから、タブ、開閉ナビ、グリッド、モーダルまで、実装と確認の要点を整理します。
コード例は考え方を示す最小構成なので、実際の画面ではコンポーネント単位で動作確認してください。

キーボード操作の土台になる三つの原則

ネイティブHTMLを最初に選ぶ

<button>、<a href>、<input>などのHTML要素には、役割、フォーカス可能性、標準のキー操作が備わっています。
見た目だけをボタンにした<div>へrole="button"を付けても、EnterキーやSpaceキーへの応答は自動では追加されません。

独自UIを作る前に、同じ目的を果たすHTML要素がないかを確認します。
ARIAはHTMLの不足を補う仕組みであり、動作そのものを実装する仕組みではありません。

名前、役割、状態を一致させる

支援技術へ正しく伝える情報は、次の三つに分けると確認しやすくなります。

  • 名前:利用者が操作対象を識別するラベルです。可視ラベルの文言をアクセシブルネームにも含めます。
  • 役割:ボタン、リンク、タブなど、その要素が何であるかを示します。
  • 状態:開閉、選択、チェック、現在値などを示します。例はaria-expanded、aria-selected、aria-checked、aria-valuenowです。

表示が変わったときは、見た目と状態属性を同じ処理で更新します。
たとえばメニューを開いたのにaria-expanded="false"が残ると、画面と読み上げの情報が食い違います。

フォーカスを動かす理由を決める

フォーカス移動は、利用者の現在地を変える操作です。
「画面が更新されたから」という理由だけで移動させず、次に操作すべき場所が変わったときに限ります。

モーダルを開く、入力エラーの要約へ案内する、ページ相当の表示へ切り替える場面では移動が役立ちます。
一方、検索件数や保存完了の通知は、フォーカスを奪わずライブリージョンで伝えるほうが自然です。

フォーカス順序とtabindexを整える

Tabキーによる順序は、意味と操作を保つ必要があります。
多くのページでは、DOMの並びを見た目の読み順と合わせると、キーボード利用者と読み上げ利用者の双方が流れを追いやすくなります。

CSSのorderや配置指定だけで要素を並べ替えると、見た目とTab順が離れる場合があります。
カードを横並びにした画面でも、見出し、説明、操作という関係がDOM上で崩れていないかを確認してください。

指定 用途 注意点
指定なし リンクやボタンなど、標準でフォーカスできる要素 通常はこの状態を優先する
tabindex="0" 要素をDOM順のTab移動へ加える 独自要素を安易に操作部品へ変えない
tabindex="-1" Tab順には入れず、スクリプトからフォーカスできるようにする 見出しやエラー要約への一時的な移動に使える
正の値 指定値を優先したTab順を作る DOMや画面変更のたびに順序が壊れやすいため避ける

フォーカス表示を消さず、隠さない

:focus-visibleは、ブラウザがフォーカス表示を必要と判断した場面へスタイルを適用する疑似クラスです。
キーボード操作では表示されることが多い一方、「マウス利用時には必ず消える」という指定ではありません。

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 3px solid #0b57d0;
  outline-offset: 3px;
}

色は一色を決めて終わりにせず、明るい背景、暗い背景、画像上、選択状態で確認します。
WCAG 2.2の達成基準2.4.7「フォーカスの可視化」はレベルAA、2.4.11「隠されないフォーカス(最低限)」もレベルAAです。
フォーカス表示の面積と変化前後の3対1を定める2.4.13「フォーカスの外観」はレベルAAAなので、AAの要件と混同しないようにします。

固定ヘッダー、Cookieバナー、画面下部の固定操作が、フォーカス中の要素を完全に覆っていないかも確認します。
必要に応じてスクロール余白を設け、拡大表示時にも現在地が見える状態を保ちます。

スキップリンクで反復部分を飛ばす

スキップリンクは、ヘッダーやナビゲーションを繰り返し通らず、本文などへ移動するリンクです。
ページの早い位置に置き、フォーカスを受けたときに画面内へ表示します。

<a class="skip-link" href="#main-content">本文へ移動</a>
<main id="main-content" tabindex="-1">…</main>
.skip-link {
  position: fixed;
  inset-block-start: .5rem;
  inset-inline-start: .5rem;
  transform: translateY(-200%);
}
.skip-link:focus {
  transform: translateY(0);
}

mainはランドマークとして機能します。
複数のnavやasideがある場合は、それぞれの目的が区別できる名前をaria-labelまたはaria-labelledbyで付けます。

ロービングtabindexを使う場面

ロービングtabindexとは、複数項目のうち一つだけをTab順に置き、矢印キーで内部の項目を移動する実装方法です。
タブリストやアプリケーションメニューなどの複合ウィジェットで、Tab停止位置が必要以上に増えるのを防ぎます。

リンクが並ぶだけのナビゲーションや通常のカード一覧には、ロービングtabindexを持ち込みません。
その場合は、すべてのリンクを通常のTab順で利用できるほうが予測しやすいからです。

UI Tabキー 領域内の主なキー
タブ 選択中のタブへ入り、次のTabで外へ出る 左右矢印、必要に応じてHomeとEnd。手動選択ならEnterまたはSpace
開閉ナビ ボタンとリンクを通常順で移動する EnterまたはSpaceで開閉し、Escapeで閉じる。矢印移動は任意
メニュー アプリケーション型のメニュー全体へ入る 矢印で項目間を移動し、Enterで実行し、Escapeで閉じる
グリッド グリッド全体を一つの停止位置として扱う 上下左右の矢印でセルを移動する

タブは自動選択と手動選択を分ける

タブへフォーカスした時点でパネルを切り替える方式が自動選択です。
パネルを待ち時間なく表示できる場合に向いています。

EnterキーまたはSpaceキーを押してから切り替える方式が手動選択です。
パネル表示に通信や大きな処理が必要なら、矢印でタブを探すたびに待たせない手動選択が適しています。

<div role="tablist" aria-label="請求情報">
  <button id="tab-detail" role="tab" aria-selected="true"
          aria-controls="panel-detail" tabindex="0">明細</button>
  <button id="tab-payment" role="tab" aria-selected="false"
          aria-controls="panel-payment" tabindex="-1">支払い方法</button>
</div>
<div id="panel-detail" role="tabpanel" aria-labelledby="tab-detail" tabindex="0">…</div>
<div id="panel-payment" role="tabpanel" aria-labelledby="tab-payment" tabindex="0" hidden>…</div>
const tablist = document.querySelector('[role=tablist]');
const tabs = [...tablist.querySelectorAll('[role=tab]')];

function activateTab(nextTab, moveFocus = true) {
  tabs.forEach((tab) => {
    const selected = tab === nextTab;
    tab.tabIndex = selected ? 0 : -1;
    tab.setAttribute('aria-selected', String(selected));
    document.getElementById(tab.getAttribute('aria-controls')).hidden = !selected;
  });
  if (moveFocus) nextTab.focus();
}

tablist.addEventListener('keydown', (event) => {
  const current = tabs.indexOf(document.activeElement);
  const destinations = {
    ArrowRight: (current + 1) % tabs.length,
    ArrowLeft: (current - 1 + tabs.length) % tabs.length,
    Home: 0,
    End: tabs.length - 1
  };
  if (destinations[event.key] === undefined) return;
  event.preventDefault();
  activateTab(tabs[destinations[event.key]]);
});

tabs.forEach((tab) => {
  tab.addEventListener('click', () => activateTab(tab, false));
});

この例はパネルをすぐ表示できる自動選択です。
複数のタブリストがある画面では、処理をコンポーネントごとにスコープし、縦向きではaria-orientation="vertical"と上下矢印を使います。

サイトの開閉ナビにmenuロールを付けない

一般的なサイトナビゲーションは、リスト、リンク、開閉用ボタンの組み合わせで作れます。
role="menu"とrole="menuitem"は、デスクトップアプリのような操作規則を持つメニュー向けです。

<nav aria-label="主要メニュー">
  <button aria-expanded="false" aria-controls="product-links">製品</button>
  <ul id="product-links" hidden>
    <li><a href="/product/a">製品A</a></li>
    <li><a href="/product/b">製品B</a></li>
  </ul>
</nav>

開いた後もリンクは通常のTab順で移動でき、Escapeキーで閉じたら開閉ボタンへ戻す設計にします。
矢印キーを追加する場合も、Tabキーによる標準操作を失わせないようにします。

カード一覧とグリッドを区別する

role="grid"は、表計算や日付選択のように行と列を矢印キーで移動する対話型UI向けです。
カードを並べるためだけに指定すると、利用者が期待するキー操作まで実装する必要が生じます。

通常のカード一覧には<ul>と<li>を使い、カード内のリンクやボタンを自然なDOM順に置きます。
グリッドを選ぶ場合は、端で止めるか反対側へ循環するかを決め、選択状態とフォーカス状態を分けて管理します。

モーダルのフォーカス管理

モーダルダイアログは、閉じるまで背後の画面を操作できない対話領域です。
実装では、開く、内部を移動する、Escapeキーで閉じる、閉じた後に戻る、という一連の流れを設計します。

  1. 開く直前のフォーカス位置を保存する。
  2. ダイアログを表示し、内容に応じた要素へフォーカスを移す。
  3. TabキーとShiftキーを押しながらのTabキーをダイアログ内で循環させる。
  4. 画面内に見える閉じるボタンを用意し、Escapeキーでも閉じられるようにする。
  5. 閉じたら起点へ戻す。起点が消えた場合は、次の作業につながる要素へ移す。

初期フォーカスは、常に最初の入力欄とは限りません。
長い説明や表を含む場合は冒頭の見出し、取り消しにくい操作では安全側のボタンなど、内容を理解してから操作できる位置を選びます。

<button id="open-settings">設定を開く</button>
<dialog id="settings-dialog" aria-labelledby="settings-title">
  <h2 id="settings-title" tabindex="-1">通知設定</h2>
  <label><input type="checkbox"> メール通知を受け取る</label>
  <button id="close-settings">閉じる</button>
</dialog>
const openButton = document.getElementById('open-settings');
const closeButton = document.getElementById('close-settings');
const dialog = document.getElementById('settings-dialog');
const dialogTitle = document.getElementById('settings-title');
let returnTarget;

openButton.addEventListener('click', () => {
  returnTarget = document.activeElement;
  dialog.showModal();
  dialogTitle.focus();
});

closeButton.addEventListener('click', () => dialog.close());
dialog.addEventListener('close', () => {
  if (returnTarget?.isConnected) returnTarget.focus();
});

showModal()で開いた<dialog>では、ブラウザが背後を不活性にします。
独自ダイアログでaria-hidden="true"だけを背後へ付けても、フォーカス移動や操作は止まりません。
対応環境や要件のために独自実装が必要なら、フォーカストラップ、背景の不活性化、表示、読み上げをまとめて検証できる実装を選びます。

より細かな初期フォーカスや閉じる条件は、アクセシブルなモーダルダイアログの設計で確認できます。

更新結果とエラーを伝える

画面の一部だけが更新されると、見た目では変化が分かっても、読み上げ利用者には伝わらない場合があります。
保存完了や検索件数のように次の操作位置を変えない情報は、role="status"などのライブリージョンへ表示します。

<p id="save-status" role="status" aria-atomic="true"></p>
document.getElementById('save-status').textContent = '保存が完了しました。';

ライブリージョンは、更新前からDOM内に置いておくと変化を検出しやすくなります。
同じメッセージを何度も読み上げたり、軽い通知へrole="alert"を多用したりしないよう、通知の優先度を分けます。

送信時に複数の入力エラーがある場合は、見えるエラー要約へtabindex="-1"を付け、送信後にフォーカスを移す方法があります。
要約から各入力欄へ移動できるリンクを用意し、個別のエラー説明はaria-describedbyで入力欄と関連付けます。

ショートカットとポインター操作の代替

ショートカットは追加機能として設計する

独自のショートカットだけに機能を置かず、画面上のボタンやメニューからも同じ操作を実行できるようにします。
ブラウザ、OS、入力支援ソフトウェアの予約キーと衝突しないかを調べ、利用者が確認できる一覧と設定を用意します。

aria-keyshortcutsは割り当てを支援技術へ伝える属性であり、キー操作そのものは実装しません。
文字、数字、記号だけの単一キーショートカットは、無効化、修飾キーを含む組み合わせへの変更、対象にフォーカスがある間だけ有効化のいずれかを提供します。

<button aria-keyshortcuts="Alt+Shift+F">検索を開く</button>
<p>ショートカット:Alt、Shift、F</p>

ドラッグやホバーと同じ結果へ到達できるようにする

  • 並べ替えには、ドラッグに加えて「上へ」「下へ」などのボタンを用意する。
  • ホバーで開く補足はフォーカスでも表示し、Escapeキーなどで閉じられるようにする。
  • 長押しだけで実行する機能には、クリック、Enterキー、Spaceキーなどの代替を用意する。

WCAG 2.2の2.5.7「ドラッグ動作」はポインター操作の代替を求める基準です。
キーボード操作については、別に2.1.1「キーボード」で確認します。

WCAG 2.2との対応を確認する

この記事の実装項目は、主に次の達成基準と関係します。
WCAG 2.2で追加された達成基準の実務上の読み方もあわせて参照できます。

達成基準 レベル この記事で扱う確認内容
2.1.1 キーボード A 機能をキーボードから実行できるか
2.1.2 キーボードトラップなし A 通常の方法でコンポーネントから移動できるか
2.1.4 文字キーのショートカット A 単一文字キーを無効化、変更または限定できるか
2.4.1 ブロックの回避 A 反復する領域を飛ばす仕組みがあるか
2.4.3 フォーカス順序 A 移動順が意味と操作を保つか
2.4.7 フォーカスの可視化 AA キーボードフォーカスを目で確認できるか
2.4.11 隠されないフォーカス(最低限) AA フォーカス対象が固定要素などに完全に隠れないか
1.4.11 非テキストのコントラスト AA 部品や状態を識別する視覚情報に必要なコントラストがあるか
2.5.7 ドラッグ動作 AA ドラッグなしでも同じ結果へ到達できるか
2.5.8 ターゲットのサイズ(最低限) AA 例外を除き24×24 CSSピクセル以上、または十分な間隔があるか
4.1.2 名前、役割、値 A 独自UIの意味と状態が支援技術へ伝わるか
4.1.3 ステータスメッセージ AA フォーカスを移さず更新結果を伝えられるか

この対応表やコード例だけで、ページ全体のWCAG適合を宣言することはできません。
適合性は、対象範囲を定め、ほかの達成基準を含めて実際のコンテンツと利用環境を試験した結果から判断します。

リリース前のキーボードスモークテスト

  1. マウスを使わず、TabキーとShiftキーを押しながらのTabキーでページを往復する。
  2. すべてのリンク、ボタン、入力欄へ到達でき、フォーカス位置を見失わないことを確認する。
  3. フォーカス順が画面の意味と合い、正のtabindexで飛び回っていないことを確認する。
  4. スキップリンクで本文へ移動し、固定ヘッダーやバナーに着地点が隠れないことを確認する。
  5. タブ、開閉ナビ、グリッドを、それぞれ定めた矢印キー、Enterキー、Spaceキー、Escapeキーで操作する。
  6. モーダルの外へTabキーが抜けず、閉じた後に起点または次の自然な操作位置へ戻ることを確認する。
  7. 保存、検索、入力エラーなどの更新内容を、画面表示とスクリーンリーダーの双方で確認する。
  8. 拡大表示、明暗の異なる配色、タッチ操作でもフォーカスや操作対象が欠けないことを確認する。

これは短時間で重大な問題を見つけるための確認であり、完全な適合試験の代わりではありません。
自動検査は属性の欠落などを見つける助けになりますが、順序の自然さや閉じた後の戻り先は人が操作して判断します。

運用で再発を防ぐ

フォーカス管理を画面ごとの個別対応にすると、同じ問題が別の機能で繰り返されます。
コンポーネント仕様へ、使用するHTML要素、名前と状態、対応キー、初期フォーカス、終了後の戻り先を記録します。

  • デザインでは、通常時、ホバー時、フォーカス時、無効時の見え方を定義する。
  • 実装では、DOM順、標準キー、状態属性、背景の不活性化を受け入れ条件にする。
  • QAでは、キーボード操作表に沿った往復テストと支援技術での確認を行う。
  • 改修時は、共通コンポーネントの変更が利用画面へ与える影響を再確認する。

共通化の進め方は、アクセシブルなデザインシステムの実践ガイドで詳しく整理しています。

キーボードで迷わないUIにするために

最初に直すのは、Tab順、フォーカス表示、標準HTMLの選択です。
そのうえで、タブやグリッドには複合ウィジェットの操作規則を、モーダルには開閉を通したフォーカスの流れを実装します。

最後に、通知では不要にフォーカスを奪わず、ショートカットやドラッグには代替手段を残します。
キーボードで最初から最後まで操作すると、個々の属性チェックだけでは見つけにくい「途中で迷う箇所」が見えてきます。

キーボードで迷わない導線は、実装後も点検を続けてこそ保てます。株式会社greedenは、UUUウェブアクセシビリティサービスとチェックを通じ、既存サイトの使いにくさを見つけて改善を支援します。

投稿者 greeden

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)