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

キーボード操作とフォーカス管理のアクセシビリティ完全ガイド:順序・可視化・ロービングTabIndex・ショートカット・モーダル/タブ/メニューまで

概要サマリー(先に要点)

  • Tab順=論理順を守り、最初のTabでスキップリンク→本文に入れる“地図”を用意します。
  • フォーカス可視化は太く・コントラスト十分にし、:focus-visible を活用。非テキスト3:1を満たします。
  • 複数子要素を持つUI(タブ・メニュー・グリッド)はロービングTabIndexTab=領域間/矢印=領域内に。
  • モーダル開閉・フォーカストラップ・Esc閉じ・復帰の4点セット。inert/aria-hiddenで背景を静めます。
  • ショートカットは宣言と衝突回避を徹底。必ず**代替操作(クリック/キーボード標準)**を提供し、学習しやすい記号化を。
  • 5分で回せるスモークテストと、現場で使える実装スニペット(HTML/ARIA/CSS/JS)を多数収録。

対象読者(具体):フロントエンドエンジニア、UI/UXデザイナー、プロダクトマネージャー、QA、テクニカルライター、Web編集者、デザインシステム運用担当
アクセシビリティレベルWCAG 2.1 AA 準拠(可能箇所は 2.2/2.5 の拡張項目も推奨)


1. はじめに:アクセシビリティは“フォーカスの旅路”から

利用者は、Tabキーでリンクやボタンを巡り、矢印キーで項目を選び、Enter/Spaceで実行します。スクリーンリーダーに限らず、トラックパッドや外付けキーボード、口で操作するスティック、スイッチコントロールなど、多様な入力手段でWebは使われていますの。
ところが、見えないフォーカス飛び回る順序閉じられないモーダルは、誰にとってもストレスの源。順序・可視化・操作の一貫性が整うだけで、体験は劇的に落ち着きます。
本記事では、原則→代表パターン→コード→テスト→運用の順で、実務にそのまま使える“型”をお届けしますね。


2. 大原則:順序・名前・見え方

2.1 Tab順=論理順

  • DOM順=読み順=Tab順が基本。視覚レイアウトで並べ替えるならソース順を優先し、CSSのorder等で視覚だけを変えます。
  • tabindex="0"フォーカス可能化に、正の値(1,2…)は原則禁止(順序の崩壊を招きます)。

2.2 名前・役割・値(NRV)

  • 名前:可視ラベル=アクセシブルネーム(2.5.3 Label in Name)。
  • 役割button / tab / tabpanel / menuitem などネイティブか相当ARIA
  • aria-checked/aria-expanded/aria-selected/valueNow 等で状態を露出

2.3 フォーカスの「見え方」

  • 太さ3px以上アウトラインオフセット2–3px背景とのコントラストを確保。
  • :focus-visibleキーボード時のみ明確表示、マウス時は控えめに。
:focus-visible {
  outline: 3px solid #FF9900; 
  outline-offset: 3px;
}

3. スキップリンクとランドマーク:最初の“一歩”を短く

  • ページ冒頭にスキップリンクを置き、本文(main)へ移動できるように。
  • 複数のnavasideには**aria-label**で役割名(例:「グローバル」「フッター」)を。
<a class="skip" href="#content">本文へスキップ</a>
<main id="content" tabindex="-1">…</main>
.skip{ position:absolute; left:-9999px; }
.skip:focus{ left:1rem; top:1rem; background:#111; color:#fff; padding:.5rem .75rem; border-radius:.5rem; }

4. ロービングTabIndex:複合コンポーネントの定石

狙いTabコンポーネント全体を移動、矢印内部アイテムを移動して、Tabストリームを短く保つ。

4.1 タブ(Tabs)

  • 構造role="tablist" 内の button[role="tab"]、対応する div[role="tabpanel"]
  • キーボード:左右矢印で隣のタブ、Home/Endで先頭/末尾、Space/Enterで選択。
<div role="tablist" aria-label="請求情報" id="tabs">
  <button role="tab" aria-selected="true"  aria-controls="p1" id="t1" tabindex="0">明細</button>
  <button role="tab" aria-selected="false" aria-controls="p2" id="t2" tabindex="-1">支払い方法</button>
  <button role="tab" aria-selected="false" aria-controls="p3" id="t3" tabindex="-1">設定</button>
</div>
<section id="p1" role="tabpanel" aria-labelledby="t1">…</section>
<section id="p2" role="tabpanel" aria-labelledby="t2" hidden>…</section>
<section id="p3" role="tabpanel" aria-labelledby="t3" hidden>…</section>
const tabs = document.querySelectorAll('[role="tab"]');
const panels = document.querySelectorAll('[role="tabpanel"]');

function activateTab(tab){
  tabs.forEach(t=>{ t.setAttribute('tabindex', t===tab? '0':'-1'); t.setAttribute('aria-selected', String(t===tab)); });
  panels.forEach(p=> p.hidden = (p.id !== tab.getAttribute('aria-controls')));
  tab.focus();
}
document.getElementById('tabs').addEventListener('keydown', e=>{
  const list = [...tabs]; const i = list.indexOf(document.activeElement);
  if(e.key==='ArrowRight') activateTab(list[(i+1)%list.length]);
  if(e.key==='ArrowLeft')  activateTab(list[(i-1+list.length)%list.length]);
  if(e.key==='Home')       activateTab(list[0]);
  if(e.key==='End')        activateTab(list[list.length-1]);
});
tabs.forEach(t=> t.addEventListener('click', ()=>activateTab(t)));

4.2 メニュー(Disclosure型ナビ)

  • トリガ<button aria-expanded>で、開くと最初の項目tabindex="0"
  • 矢印で前後移動、Escで閉じてトリガに復帰
<div class="menu">
  <button id="menuBtn" aria-expanded="false" aria-controls="m1">アカウント</button>
  <ul id="m1" role="menu" hidden>
    <li><a role="menuitem" href="/profile" tabindex="0">プロフィール</a></li>
    <li><a role="menuitem" href="/settings" tabindex="-1">設定</a></li>
    <li><button role="menuitem" tabindex="-1">ログアウト</button></li>
  </ul>
</div>
const btn = document.getElementById('menuBtn'), menu = document.getElementById('m1');
const items = ()=> [...menu.querySelectorAll('[role="menuitem"]')];
btn.addEventListener('click', ()=>{
  const open = btn.getAttribute('aria-expanded')==='true';
  btn.setAttribute('aria-expanded', String(!open));
  menu.hidden = open;
  if(!open) items()[0].focus();
});
menu.addEventListener('keydown', e=>{
  const list = items(); const i = list.indexOf(document.activeElement);
  if(e.key==='ArrowDown') list[(i+1)%list.length].focus();
  if(e.key==='ArrowUp')   list[(i-1+list.length)%list.length].focus();
  if(e.key==='Escape'){ btn.click(); btn.focus(); }
});

注意:サイトのナビにはネイティブul/li/a+ボタン開閉で十分。role="menu"はアプリメニューの意味が強いため乱用は避けるのが安全です。

4.3 グリッド(カード群・日付ピッカー)

  • グリッドrole="grid"、セルにrole="gridcell"。行/列の矢印移動を実装し、選択はEnter/Space

5. モーダルダイアログ:開閉・トラップ・復帰・告知

5.1 必須の4点セット

  1. 開く:最初に操作すべき要素へフォーカス移動
  2. トラップ:ダイアログ内でTab循環、背景は**inert**(対応外はaria-hidden="true")。
  3. Esc閉じEscapeで閉じる。
  4. 復帰:閉じたらトリガ要素へフォーカスを戻す。
<button id="open">設定を開く</button>
<dialog id="dlg" aria-labelledby="dlg-title">
  <h2 id="dlg-title">通知設定</h2>
  <label><input type="checkbox"> メール通知を受け取る</label>
  <div class="actions">
    <button id="save">保存</button>
    <button id="close">閉じる</button>
  </div>
</dialog>
const openBtn = document.getElementById('open');
const dlg = document.getElementById('dlg'); 
let lastFocus;
openBtn.addEventListener('click', ()=>{
  lastFocus = document.activeElement;
  dlg.showModal(); // <dialog>が使えない環境ではロールバック実装を
  dlg.querySelector('input,button,select,textarea,[tabindex="0"]')?.focus();
});
document.getElementById('close').addEventListener('click', ()=>{ dlg.close(); lastFocus?.focus(); });
dlg.addEventListener('cancel', (e)=>{ e.preventDefault(); dlg.close(); lastFocus?.focus(); }); // Esc

<dialog> 非対応時は、背景に aria-hidden="true" を付与、bodyoverflow:hiddenにし、ダイアログ内でフォーカストラップを実装します。


6. フォーカス可視のデザイン:見失わせない

  • 非テキスト3:1(枠・アイコン・フォーカスリング)を満たす色設計。
  • 太さ:2–3px、オフセット:2–4pxで“にじみ”を回避。
  • ダーク/ライトの両テーマで見える色を用意し、prefers-contrast: more で太さ強化。
:root{ --focus:#FF9900; }
:focus-visible{ outline:3px solid var(--focus); outline-offset:3px; }
@media (prefers-contrast: more){
  :focus-visible{ outline-width:4px; }
}

7. ショートカット設計:速く、でも“必ず学べて代替可能”

  • 必ず代替操作(ボタン/メニュー/通常のキーボード操作)を提供。
  • 記法を統一(例:Win/Linux=Ctrl、macOS=⌘)。
  • 衝突回避:ブラウザ予約(例:Ctrl+L, Ctrl+T, ⌘+R)は避ける。
  • 表示:ヘルプやツールチップ、メニュー項目に併記
<button aria-keyshortcuts="Ctrl+K" aria-label="検索(Ctrl+K)">検索</button>
window.addEventListener('keydown', (e)=>{
  const mac = navigator.platform.includes('Mac');
  if((mac? e.metaKey : e.ctrlKey) && e.key.toLowerCase()==='k'){ e.preventDefault(); document.getElementById('search').focus(); }
});

8. フォーカス順序の設計パターン:実践集

8.1 カード一覧 → 詳細

  • 一覧(role="region"カードの見出しリンク内部の主要操作→次のカード。
  • 「もっと見る」は最後に。見出し→本文→操作の順で反復パターンを作る。

8.2 フィルタと結果

  • フィルタを先に置く(スキップリンクで結果へ飛ぶ導線も)。
  • 適用→結果にフォーカスで“反応があった”ことを示す(role="status")。

8.3 エラー時

  • エラー要約role="alert")へフォーカス移動各項目のリンクで現場へジャンプ。

9. マウス/タッチ前提UIの代替:ドラッグ・ホバー・長押し

  • ドラッグ操作の代替:ボタン(上へ/下へ/左へ/右へ)やメニューで同じ結果を提供(WCAG 2.5.7)。
  • ホバー依存フォーカス時にも表示(ツールチップ、メガメニュー)。
  • 長押しクリック/Enterで代替、時間制約を緩く。
<div role="listbox" aria-label="並び順">
  <button aria-label="1つ上へ">↑</button>
  <button aria-label="1つ下へ">↓</button>
</div>

10. ライブ地域と状態通知:静かに、確実に伝える

  • role="status":保存完了など非割り込み
  • role="alert":エラーや重要障害など割り込み
  • aria-live="polite":検索ヒット数など自動更新の実況。
<div id="status" role="status" aria-atomic="true" class="sr-only"></div>
<script>
function saved(){ document.getElementById('status').textContent='保存が完了しました。'; }
</script>

11. よくある落とし穴と回避策

落とし穴 何が起きる? 回避策
tabindexの正値乱用 順序崩壊・迷子 DOM順の設計を見直し、tabindex="0"のみに
フォーカスリングを消す 見失う :focus-visibleで美しく残す
モーダルで背景操作可 二重フォーカス・混乱 inert/aria-hidden+トラップ+Esc+復帰
矢印無効のタブ 学習コスト↑ ロービングTabIndex+左右/Home/End
ホバーだけ開閉 キーボード不可 ボタンで開閉、aria-expanded
ダイアログをdisplay:noneで隠すだけ SRに残る/消える問題 開閉時に適切な属性フォーカス遷移
ショートカットだけ 学習不能 メニュー・ボタン・説明と重複提供

12. 5分スモークテスト:毎リリースの最小儀式

  1. Tabのみで主要機能を完遂(開く→操作→閉じる→次へ)。
  2. 最初のTabでスキップリンクが現れ、本文に着地
  3. タブ/メニュー:左右/上下矢印で内部移動Home/End が効く。
  4. モーダル:開く→最初の操作にフォーカス→Esc→トリガに復帰
  5. フォーカスリング常に見える(ライト/ダーク両方)。
  6. ショートカット:ヘルプに一覧、代替操作あり、ブラウザ予約と衝突しない。
  7. SR(NVDA/VoiceOver)名前・役割・値が読み上げられる。

13. コードスニペット集(コピペOK)

13.1 フォーカス可視のトークン

:root{ --focus-color:#FF9900; --focus-width:3px; --focus-offset:3px; }
:focus-visible{ outline: var(--focus-width) solid var(--focus-color); outline-offset: var(--focus-offset); }

13.2 inertフォールバック(簡易)

function setInert(el, inert){
  if('inert' in HTMLElement.prototype){ el.inert = inert; }
  else { el.setAttribute('aria-hidden', String(inert)); }
}

13.3 グリッドの矢印移動(骨子)

function moveInGrid(e, cols){
  const items = [...document.querySelectorAll('[role="gridcell"]')];
  const i = items.indexOf(document.activeElement);
  const map = { ArrowRight: i+1, ArrowLeft: i-1, ArrowDown: i+cols, ArrowUp: i-cols };
  if(map[e.key] !== undefined){ items[(map[e.key]+items.length)%items.length]?.focus(); e.preventDefault(); }
}

13.4 “戻る場所”の保存と復帰

let lastFocus;
function rememberFocus(){ lastFocus = document.activeElement; }
function restoreFocus(){ lastFocus?.focus(); }

14. ケーススタディ:フォーカスの整理だけで離脱が減った話

Before

  • フォーカスリングは消去。
  • メガメニューはホバーで開閉、キーボードでは到達不能。
  • モーダルは背景が操作可能で、Escも効かない。

After

  1. :focus-visible太い輪郭を導入。
  2. メガメニューをボタン開閉+矢印移動に刷新。
  3. モーダルをトラップ+Esc+復帰に改修、背景をinert
    結果:フォーム完了率 +14%、SRユーザーからの「操作に迷う」指摘 −76%、ヘルプ参照率 −28%

15. 組織導入:デザインシステムに“動線のルール”を刻む

  • コンポーネント仕様キーボード操作表(Tab/矢印/Home/End/Esc/Enter/Space)を必ず添付。
  • フォーカストークン(色・太さ・オフセット)を全要素共通で定義。
  • **“Doneの定義”**に、スキップリンク・フォーカス順・モーダルの4点セット・ロービングTabIndex・ショートカットの代替を追加。
  • 自動検査+5分スモークをPRテンプレへ。リグレッションを最小化します。

16. 対象読者と導入メリット(具体)

  • フロントエンドエンジニア:ロービングTabIndexやモーダル制御の定石化で実装が安定、回帰バグが激減
  • UI/UXデザイナーフォーカス可視のデザイン言語が整い、アクセシビリティとブランド表現を両立。
  • PM/ディレクターAA準拠の受入基準が明文化され、品質ゲートが明確に。
  • QA/CS:テスト観点が定型化、問い合わせの**「操作に迷う」系**が減少。
  • 利用者:キーボード/支援技術/スイッチ操作でも迷わず・確実にタスク完了。

17. アクセシビリティレベルの評価(本記事の到達点)

  • WCAG 2.1 AA(主な対応)
    • 2.1.1 キーボード:すべての機能がキーボードで操作可能
    • 2.4.3 フォーカス順序:論理順の確保
    • 2.4.7 フォーカス可視:太さ・色・オフセットで明確化
    • 2.4.1 ブロックの回避:スキップリンク
    • 4.1.2 名前・役割・値aria-*で状態・関係を露出
    • 1.4.11 非テキストコントラスト:フォーカスリング・境界の3:1
    • 2.2.2 一時停止・停止・非表示:モーダルやメニューの開閉制御
  • WCAG 2.2(推奨)
    • 2.5.7 Dragging Movements:ドラッグの代替
    • 2.5.8 Target Size (Minimum):操作ターゲットの最小サイズ(44×44px目安)

18. まとめ:フォーカスは“やさしい光”

  1. Tab順=論理順、最初のTabで本文へ
  2. フォーカスリングは太く・高コントラスト、:focus-visibleで気持ちよく。
  3. ロービングTabIndexで、複合UIの操作規則を統一。
  4. モーダルの4点セット(開く・トラップ・Esc・復帰)を必ず。
  5. ショートカットは表示・代替・衝突回避で“優しい高速”。
  6. 5分スモークデザインシステムで、品質を継続して育てる。

フォーカスの旅路が整えば、どんな入力手段でも同じ理解と結果へ辿り着けます。あなたのUIが、静かに道しるべを灯す存在になりますように。わたしも心を込めてお手伝いしますね。


投稿者 greeden

コメントを残す

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

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