Green key with wheelchair icon on white laptop keyboard. Accessibility disability computer symbol
目次

キーボードだけで使えるナビゲーション設計 完全ガイド:Tab一つで迷わないUIをつくる

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

  • すべての操作をキーボードだけで完結できることがアクセシビリティの土台
  • DOM順=視覚順、フォーカスが常に見える予測できる移動が三本柱
  • 役割に合ったネイティブ要素を優先(buttonanavul/li)し、必要最小限のARIA
  • スキップリンク/ランドマーク:focus-visibleは必須セット
  • メニュー・タブ・モーダルなど主要コンポーネントのキー操作仕様と実装サンプル
  • アンチパターン回避・手動テスト手順・運用PDCAで品質を保つ
  • 想定読者と導入効果、到達目標(WCAG 2.1 AA)を明確化

1. はじめに:なぜ「キーボードだけで使える」ことが最優先なの?

マウスが使えない状況や、スクリーンリーダー・スイッチ操作・音声入力を使う場面は想像以上に多くあります。キーボード操作への最適化は、障がいの有無にかかわらず誰もが迷わず操作できるUIをもたらします。さらに、フォーカス順やナビゲーションが整うと、認知的負荷の軽減作業効率の向上離脱率の低下といった効果が期待できます。
本ガイドでは、AA準拠(後述)を視野に、ナビゲーションをキーボードだけで完結させる設計・実装・検証を、具体的なコードとともに丁寧に解説しますね。


2. 原則:この3つを守れば強い

  1. DOM順=視覚順
    見えている順にTabが進めば迷いません。CSSで視覚順だけを入れ替える(orderposition乱用など)は避け、レイアウトとDOM順を一致させます。
  2. フォーカスは常に見える
    :focus-visibleでコントラストの高い輪郭を表示。outlineを消さないのが鉄則です。
  3. 予測できる移動
    似たコンポーネントでは同じキー操作が効くこと(タブなら矢印、メニューなら矢印+Escなど)。慣れがそのまま操作性になります。

3. 基礎セット:スキップリンク・ランドマーク・見出し

3.1 スキップリンク(最短で主要内容へ)

<a class="skip" href="#main">本文へスキップ</a>
<header>…</header>
<nav aria-label="主なナビゲーション">…</nav>
<main id="main" tabindex="-1">…</main>
.skip {
  position:absolute; left:-9999px; top:auto;
}
.skip:focus {
  left:16px; top:16px; z-index:1000;
  background:#fff; color:#000; padding:.5em .75em;
  outline:3px solid #ff9900;
}
  • maintabindex="-1"を与えると、スキップ時に確実にフォーカスを当てられます。

3.2 ランドマークと見出しで「地図」をつくる

  • header / nav / main / aside / footer を適切に使い、必要に応じて aria-label
  • 見出し階層(h1h2h3…)を正しく。スクリーンリーダーは見出しジャンプでページを素早く探索します。

3.3 :focus-visibleで「今どこ?」を常に明示

:focus-visible {
  outline:3px solid #ff9900;
  outline-offset:2px;
  border-radius:4px;
}
  • マウス操作では表示されにくく、キーボード操作で確実に見えるのがfocus-visibleの利点です。

4. tabindex・役割・ラベル:必要最小限で堅牢に

  • tabindex="0":本来フォーカス不可能な要素をTab対象にする時のみ。乱用禁止。
  • tabindex="-1":プログラムからフォーカスを当てたい時(スキップリンク先、モーダルのヘッダなど)。
  • tabindex正の値は使用しない:Tab順が壊れ、保守不能になります。
  • ラベルは可視テキストと一致(「ボタンに書いてある文字=アクセシブルネーム」)。
  • ネイティブ要素優先:リンクは<a href>, ボタンは<button><div role="button">は最後の手段。

5. コンポーネント別:期待されるキー操作と実装

5.1 グローバルナビ(水平メニュー/ドロップダウン)

期待される操作

  • Tab/Shift+Tab:親メニュー間を移動
  • Enter/Space:サブメニューの開閉
  • ↓↑(開いた状態):サブメニュー内を移動
  • Esc:サブメニューを閉じて親へフォーカス

推奨構造(サイトナビはrole="menu"を付けないのが原則。文書用のul/libuttonで十分)

<nav aria-label="主なナビゲーション">
  <ul class="gnav">
    <li>
      <button aria-expanded="false" aria-controls="products-sub">製品</button>
      <ul id="products-sub" hidden>
        <li><a href="/products/a">製品A</a></li>
        <li><a href="/products/b">製品B</a></li>
      </ul>
    </li>
    <li><a href="/pricing">価格</a></li>
    <li><a href="/support">サポート</a></li>
  </ul>
</nav>
const toggles = document.querySelectorAll('nav button[aria-controls]');
toggles.forEach(btn => {
  const sub = document.getElementById(btn.getAttribute('aria-controls'));
  btn.addEventListener('click', () => {
    const open = btn.getAttribute('aria-expanded') === 'true';
    btn.setAttribute('aria-expanded', String(!open));
    sub.hidden = open;
    if (!open) sub.querySelector('a')?.focus();
  });
  btn.addEventListener('keydown', e => {
    if (e.key === 'Escape') { btn.setAttribute('aria-expanded','false'); sub.hidden = true; btn.focus(); }
  });
});
  • ポイント
    • 親はbutton(リンクではない)。
    • サブメニューは表示時のみTab対象(hiddenで除外)。
    • Escで閉じる。開いたら最初の項目にフォーカス

5.2 ハンバーガーメニュー(モバイル)

期待される操作

  • Enter/Spaceで開閉、Escで閉じる。
  • 開いたらメニュー全体にフォーカストラップ、閉じたら開く前に居た場所へ戻す。
<button id="menu-toggle" aria-controls="drawer" aria-expanded="false" aria-label="メニューを開く">☰</button>
<aside id="drawer" role="dialog" aria-modal="true" hidden>
  <nav aria-label="モバイルメニュー">
    <a href="/home">ホーム</a>
    <a href="/news">お知らせ</a>
    <a href="/contact">お問い合わせ</a>
    <button id="close">閉じる</button>
  </nav>
</aside>
const toggle = document.getElementById('menu-toggle');
const drawer = document.getElementById('drawer');
const closeBtn = document.getElementById('close');
let lastFocus;
function openDrawer() {
  lastFocus = document.activeElement;
  drawer.hidden = false;
  toggle.setAttribute('aria-expanded','true');
  drawer.querySelector('a,button')?.focus();
  document.addEventListener('keydown', trap);
}
function closeDrawer() {
  drawer.hidden = true;
  toggle.setAttribute('aria-expanded','false');
  lastFocus?.focus();
  document.removeEventListener('keydown', trap);
}
function trap(e){
  if(e.key==='Escape'){ closeDrawer(); return; }
  if(e.key!=='Tab') return;
  const focusables = drawer.querySelectorAll('a, button, [tabindex="0"]');
  const first = focusables[0], last = focusables[focusables.length-1];
  if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
  else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
}
toggle.addEventListener('click', ()=>drawer.hidden?openDrawer():closeDrawer());
closeBtn.addEventListener('click', closeDrawer);

5.3 タブ(roving tabindex

期待される操作

  • Tab:選択中のタブにのみ到達
  • ←→(水平)/↑↓(垂直):タブ移動
  • Enter/Space:選択・パネル切替
<div role="tablist" aria-label="設定">
  <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"]')];
function activate(i, focus=true){
  tabs.forEach((t,idx)=>{
    const selected = idx===i;
    t.setAttribute('aria-selected', String(selected));
    t.tabIndex = selected?0:-1;
    const panel = document.getElementById(t.getAttribute('aria-controls'));
    panel.hidden = !selected;
  });
  if(focus) tabs[i].focus();
}
tabs.forEach((tab,i)=>{
  tab.addEventListener('keydown', e=>{
    const keys = {ArrowRight:1, ArrowLeft:-1, ArrowDown:1, ArrowUp:-1};
    if (e.key in keys){ e.preventDefault(); 
      let next = (i + keys[e.key] + tabs.length) % tabs.length; activate(next);
    } else if (e.key==='Home'){ e.preventDefault(); activate(0); }
      else if (e.key==='End'){ e.preventDefault(); activate(tabs.length-1); }
  });
  tab.addEventListener('click', ()=>activate(i,false));
});
  • ポイント:Tabは選択中タブのみに当たり、他は矢印キーで移動。読み上げも「選択状態」を伝えます。

5.4 アコーディオン(詳細の開閉)

<h3>
  <button aria-expanded="false" aria-controls="a1" id="h1">配送について</button>
</h3>
<div id="a1" role="region" aria-labelledby="h1" hidden>
  <p>…説明…</p>
</div>
document.querySelectorAll('h3 > button').forEach(btn=>{
  const panel = document.getElementById(btn.getAttribute('aria-controls'));
  btn.addEventListener('click', ()=>{
    const open = btn.getAttribute('aria-expanded')==='true';
    btn.setAttribute('aria-expanded', String(!open));
    panel.hidden = open;
  });
});
  • ポイント:見出し+buttonどこが開閉トリガか明確。開閉状態はaria-expandedで通知。

5.5 サイドバー(ツリー/階層ナビ)

小規模なら単純なul/liで十分。複雑な階層では親はbuttonで開閉、子はa
期待操作:矢印キーで行移動、Enter/Spaceで開閉またはリンク遷移、Home/Endで先頭末尾。


5.6 検索オートコンプリート

期待操作

  • 入力 → ↓で候補に入る、↑↓で移動、Enterで決定、Escで候補を閉じる。
    実装のヒント
  • role="listbox"/role="option"またはaria-autocomplete="list"の適切な組合わせ。
  • 候補が開いたら**aria-expanded="true"、現在ハイライトはaria-activedescendant**で伝える。

6. フォーカス管理:ページ遷移・モーダル・トースト

6.1 SPAのルーティング

  • ルート変更後、<h1>または<main>の先頭にプログラムでフォーカス
  • ページ先頭に「更新通知用live領域」を置き、変化を音声でも知らせると親切です。
<div aria-live="polite" aria-atomic="true" class="sr-only"></div>

6.2 モーダルダイアログ

  • role="dialog"aria-modal="true"、最初にフォーカス可能要素へ移動。
  • フォーカストラップ(Tab循環)とEscで閉じるは必須。
  • 背景はクリックで閉じても、閉じた後はトリガに戻す

6.3 トースト通知

  • role="status"(控えめ)やrole="alert"(緊急)で読み上げ。
  • 自動で消す場合も**「閉じる」ボタン**を用意し、キーボードで操作可能に。

7. 見やすいフォーカスインジケーターのデザイン

:root { --focus: #1a73e8; }
a:focus-visible, button:focus-visible, [role="tab"]:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 30%, transparent);
}
@media (prefers-contrast: more){
  :focus-visible { outline-width: 4px; }
}
  • コントラスト比を意識(背景に対して十分濃い色)。
  • 太く・オフセットを付けて被写界のノイズに埋もれないように。

8. よくあるアンチパターンと回避策

  • outline: none; でフォーカスが消える → :focus-visible上書きして見やすく。
  • tabindex正の値(例:tabindex="9999")→ 資産化不能。0か-1のみで設計。
  • <div role="button">onclick → キー対応漏れがち。<button>を使用
  • リンクにhref="#" → 目的地が不明・タブ順も混乱。本当にリンクか再考し、ボタンと役割を分ける。
  • DOM順と視覚順の乖離 → CSS再考。順序はHTMLで決める
  • 開いたサブメニューがTabで離脱できないEsc閉じる処理を実装。
  • SPAで初期フォーカスを当てない → 遷移後は**mainh1へフォーカス**。

9. 手動テスト手順(現場でそのまま使える)

  1. Tab巡回:ヘッダー→グロナビ→メイン→フッターへ論理順で移動できるか。
  2. 逆順(Shift+Tab):戻る動作でも破綻がないか。
  3. スキップリンク:最初のTabで現れ、押すと本文先頭へ
  4. メニュー:Enter/Spaceで開閉、矢印で項目移動、Escで閉じる。
  5. タブ:矢印移動、Enter/Spaceで切替、非表示パネルはTab対象外。
  6. モバイルドロワー:開いたらフォーカスが内側に留まり、Esc/閉じるで元の場所へ戻る。
  7. モーダル:開閉・トラップ・Esc・背景クリック・フォーカス復帰を全確認。
  8. 読み上げ(任意):スクリーンリーダーで見出し/ランドマーク/リンク数が期待通りか。
  9. 高コントラスト:OSやブラウザの高コントラストでフォーカスが見えるか。

10. 運用:設計→実装→レビュー→回帰テストのPDCA

  • 設計:デザインシステムにキー操作仕様を明文化(コンポーネントごとに表に)。
  • 実装:ネイティブ優先・最小ARIA。E2EテストにTab巡回を組み込む。
  • レビュー:PRテンプレに「キーボード操作確認済み」チェック項目を追加。
  • 回帰:UI改修のたびに手動テスト8項目を実施。スプリントレビューで結果共有。

11. サンプル:チェック可能な簡易ナビ(まとめ版)

<a class="skip" href="#main">本文へスキップ</a>

<header>
  <h1>サイト名</h1>
  <nav aria-label="主なナビゲーション">
    <ul class="gnav">
      <li>
        <button aria-expanded="false" aria-controls="sub1">サービス</button>
        <ul id="sub1" hidden>
          <li><a href="/consulting">コンサルティング</a></li>
          <li><a href="/training">トレーニング</a></li>
        </ul>
      </li>
      <li><a href="/about">私たちについて</a></li>
      <li><a href="/contact">お問い合わせ</a></li>
    </ul>
  </nav>
</header>

<main id="main" tabindex="-1">
  <h2>見出しタイトル</h2>
  <p>本文テキスト…</p>

  <div role="tablist" aria-label="コンテンツ切替">
    <button role="tab" aria-selected="true" aria-controls="pA" id="tA" tabindex="0">概要</button>
    <button role="tab" aria-selected="false" aria-controls="pB" id="tB" tabindex="-1">詳細</button>
  </div>
  <section id="pA" role="tabpanel" aria-labelledby="tA">…概要コンテンツ…</section>
  <section id="pB" role="tabpanel" aria-labelledby="tB" hidden>…詳細コンテンツ…</section>
</main>

<footer>© Example</footer>

(前掲のJavaScript・CSSを併用)


12. 誰が恩恵を受ける?導入効果(具体)

  • フロントエンドエンジニア:コンポーネント単位でキー操作仕様が定義されるため、実装の迷いが減り、レビューも迅速に。Tab順の事故が減って回帰不具合が激減
  • UI/UXデザイナーフォーカス状態のデザインを最初から定義することで、視覚的整合性と使いやすさが両立。ユーザビリティテストで学習コスト低下を確認しやすい。
  • QA/アクセシビリティ担当:手動テスト手順(第9章)をそのままシナリオ化でき、スプリントごとに同等品質を担保。
  • コンテンツ編集者・PM:ナビゲーションが迷わないため、滞在時間の伸長・離脱率低下が期待でき、KPI改善に直結。
  • 支援技術ユーザー(画面リーダー・スイッチ等)一貫したキー操作で快適に移動でき、情報探索が圧倒的にスムーズに。
  • 一時的制約のあるユーザー(トラックパッドが使いづらい・腕を痛めている等):キーボードだけで全機能にアクセスできる安心感。

13. アクセシビリティレベルと適合の目安(WCAG 2.1 AA)

  • 2.1.1 Keyboard:すべての機能がキーボードで操作可能。
  • 2.4.3 Focus Order:フォーカス移動が意味的順序を保つ。
  • 2.4.7 Focus Visible:キーボードフォーカスが見える。
  • 1.3.1 Info and Relationships:見出し・リスト・ランドマークで関係性を示す。
  • 2.5.3 Label in Name:ボタンの可視テキスト=アクセシブルネーム。
  • 3.2.1/3.2.2:フォーカスや入力で予期せぬ変更を起こさない(勝手に遷移しない)。
    本ガイドの実装とテストを満たせば、AA適合の強固なベースができあがります。

14. まとめ:Tab一つで迷わない体験は、思いやりの設計から

  1. DOM順・フォーカス可視・予測可能性の原則を守る。
  2. スキップリンク/ランドマーク/見出しで情報の地図を用意。
  3. メニュー・タブ・モーダルは期待されるキー操作を実装し、最小ARIAで堅牢に。
  4. アンチパターンを避け、手動テスト8項目を開発フローに常設。
  5. 運用PDCAで継続的に磨き、WCAG 2.1 AAへ。

キーボードだけで迷わず操作できることは、すべての人の安心と自信につながります。あなたのサイトが最初のTabから最後のEnterまで心地よく流れる体験になりますように、わたしも心を込めて応援しています。


付録A:実務チェックリスト(抜粋)

  • [ ] 最初のTabで「本文へスキップ」が表示/機能する
  • [ ] ヘッダー→ナビ→メイン→フッターの順に論理的に移動
  • [ ] すべてのフォーカスに明確なインジケーター
  • [ ] ドロップダウンはEnter/Spaceで開閉、Escで閉じる、開時に内部Tab
  • [ ] タブはroving tabindex・矢印移動・非表示パネルはTab対象外
  • [ ] モーダルはトラップ・Esc・復帰が機能
  • [ ] tabindexは0/-1のみ。正値は不使用
  • [ ] outlineは消さない(またはfocus-visibleで上書き)
  • [ ] 役割に合ったネイティブ要素を使用(<a>/<button>

対象読者(具体)

  • フロントエンドエンジニア:コンポーネント設計・実装・回帰テストの責任を担う方
  • UI/UXデザイナー:フォーカス状態やキーボードフローを含めた仕様を定義する方
  • QA/アクセシビリティ担当:手動テストを運用し、適合水準を管理する方
  • Webディレクター/PM:開発プロセスにAA準拠を組み込み、リスクを最小化したい方

投稿者 greeden

コメントを残す

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

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