12k
All articles

Webアプリケーションにおけるキーボードナビゲーションを改善するためのヒント

タブ順序の修正、モーダルのフォーカストラップ、ARIA属性とセマンティックHTMLの組み合わせで、キーボード対応のWebアプリを構築する方法を解説する。

OpenReplay Team
OpenReplay Team
Webアプリケーションにおけるキーボードナビゲーションを改善するためのヒント

キーボードアクセシブルなWebアプリケーションの構築は、単なるコンプライアンスの問題ではありません。すべての人にとって使いやすいインターフェースを作ることなのです。しかし、多くの開発者がフォーカス管理、壊れたタブシーケンス、アクセシブルでないカスタムコンポーネントに苦労しています。このガイドでは、実際の開発で遭遇する一般的なキーボードナビゲーションアクセシビリティの課題に対する実用的な解決策を提供します。

主要なポイント

  • DOMの構造を、CSSレイアウトではなく視覚的なタブ順序に合わせる
  • 組み込みキーボードサポートのためにセマンティックHTML要素を使用する
  • カスタムの代替手段を提供せずにフォーカスインジケーターを削除してはいけない
  • モーダルダイアログでフォーカストラップを実装し、閉じる際にフォーカスを復元する
  • キーボードナビゲーションを手動および自動化ツールでテストする
  • カスタムインタラクティブ要素にはtabindex="0"を使用し、正の値は避ける

フォーカス管理の基本の理解

タブ順序の問題

キーボードナビゲーションアクセシビリティの最も重要な側面は、論理的なタブ順序を確立することです。DOMの構造が直接フォーカスシーケンスを決定し、CSSレイアウトではありません。この不一致が主要なユーザビリティ問題を引き起こします。

よくある間違い:

<!-- 視覚的順序: ロゴ、ナビ、コンテンツ、サイドバー -->
<div class="layout">
  <div class="sidebar">...</div>  <!-- 最初にフォーカス -->
  <div class="content">...</div>  <!-- 2番目にフォーカス -->
  <nav class="navigation">...</nav> <!-- 3番目にフォーカス -->
  <div class="logo">...</div>     <!-- 最後にフォーカス -->
</div>

より良いアプローチ:

<!-- DOM順序が視覚的フローと一致 -->
<div class="layout">
  <div class="logo">...</div>
  <nav class="navigation">...</nav>
  <div class="content">...</div>
  <div class="sidebar">...</div>
</div>

論理的なDOM順序を維持しながら、CSS GridやFlexboxを使用して視覚的な配置を制御してください。

より良いナビゲーションのためのセマンティックHTML要素

ネイティブHTML要素は組み込みキーボードサポートを提供します。divやspanで機能を再作成するのではなく、これらを使用してください。

すぐに使えるインタラクティブ要素:

  • <button> アクション用
  • <a href="..."> ナビゲーション用
  • <input><select><textarea> フォームコントロール用
  • <details><summary> 展開可能なコンテンツ用

このパターンは避ける:

<div class="button" onclick="handleClick()">送信</div>

代わりにこれを使用:

<button type="button" onclick="handleClick()">送信</button>

ネイティブHTMLが必要な動作を提供できない場合は、ARIA属性を使用してアクセシビリティセマンティクスを追加してください。ただし、常にセマンティック要素を最初に検討してください。

可視フォーカススタイルの保持

フォーカスインジケーターの危機

多くの開発者が代替手段を提供せずにoutline: noneでフォーカスインジケーターを削除しています。これはキーボードナビゲーションアクセシビリティを完全に破壊します。

代替なしでこれを行ってはいけません:

button:focus {
  outline: none; /* フォーカスインジケーターを削除 */
}

カスタムフォーカススタイルを提供:

button:focus {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
}

/* より良いUXのためにfocus-visibleを使用 */
button:focus-visible {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
}

:focus-visibleを使用したモダンなフォーカス管理

:focus-visible疑似クラスは、キーボードナビゲーションが検出された場合のみフォーカスインジケーターを表示し、キーボードユーザーとマウスユーザーの両方の体験を向上させます。

/* ベーススタイル */
.interactive-element {
  outline: none;
}

/* キーボードフォーカスのみ */
.interactive-element:focus-visible {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
  box-shadow: 0 0 0 4px rgba(0, 102, 204, 0.2);
}

一般的なtabindexの間違いを避ける

tabindexの罠

0より大きいtabindex値を使用すると、混乱するナビゲーションパターンが作成されます。これらの3つの値に固執してください:

  • tabindex="0" - 要素を自然なタブ順序でフォーカス可能にする
  • tabindex="-1" - 要素をプログラム的にフォーカス可能にするが、タブ順序から除外する
  • tabindexなし - デフォルトの動作を使用

問題のあるアプローチ:

<div tabindex="1">最初</div>
<div tabindex="3">3番目</div>
<div tabindex="2">2番目</div>
<button>4番目(自然な順序)</button>

より良い解決策:

<div tabindex="0">最初</div>
<div tabindex="0">2番目</div>
<div tabindex="0">3番目</div>
<button>4番目</button>

カスタムコンポーネントをフォーカス可能にする

カスタムインタラクティブ要素を構築する際は、tabindex="0"とキーボードイベントハンドラーを追加してください:

// カスタムドロップダウンコンポーネント
const dropdown = document.querySelector('.custom-dropdown');
dropdown.setAttribute('tabindex', '0');

dropdown.addEventListener('keydown', (e) => {
  switch(e.key) {
    case 'Enter':
    case ' ':
      toggleDropdown();
      break;
    case 'Escape':
      closeDropdown();
      break;
    case 'ArrowDown':
      openDropdown();
      focusFirstOption();
      break;
  }
});

モーダルでのキーボードトラップの防止

フォーカストラップの実装

モーダルダイアログは、キーボードユーザーが背景コンテンツにタブで移動することを防ぐためにフォーカスをトラップする必要があります。堅牢な実装は以下の通りです:

class FocusTrap {
  constructor(element) {
    this.element = element;
    this.focusableElements = this.getFocusableElements();
    this.firstFocusable = this.focusableElements[0];
    this.lastFocusable = this.focusableElements[this.focusableElements.length - 1];
  }

  getFocusableElements() {
    const selectors = [
      'button:not([disabled])',
      'input:not([disabled])',
      'select:not([disabled])',
      'textarea:not([disabled])',
      'a[href]',
      '[tabindex]:not([tabindex="-1"])'
    ].join(', ');
    
    return Array.from(this.element.querySelectorAll(selectors));
  }

  activate() {
    this.element.addEventListener('keydown', this.handleKeyDown.bind(this));
    this.firstFocusable?.focus();
  }

  handleKeyDown(e) {
    if (e.key === 'Tab') {
      if (e.shiftKey) {
        if (document.activeElement === this.firstFocusable) {
          e.preventDefault();
          this.lastFocusable.focus();
        }
      } else {
        if (document.activeElement === this.lastFocusable) {
          e.preventDefault();
          this.firstFocusable.focus();
        }
      }
    }
    
    if (e.key === 'Escape') {
      this.deactivate();
    }
  }

  deactivate() {
    this.element.removeEventListener('keydown', this.handleKeyDown);
  }
}

モーダル閉じた後のフォーカス復元

常にモーダルを開いた要素にフォーカスを戻してください:

let previousFocus;

function openModal() {
  previousFocus = document.activeElement;
  const modal = document.getElementById('modal');
  const focusTrap = new FocusTrap(modal);
  focusTrap.activate();
}

function closeModal() {
  focusTrap.deactivate();
  previousFocus?.focus();
}

キーボードナビゲーションのテスト

手動テストのチェックリスト

  1. インターフェース全体をタブで移動 - すべてのインタラクティブ要素に到達できますか?
  2. フォーカスインジケーターの確認 - 見やすく明確ですか?
  3. モーダルダイアログのテスト - フォーカストラップは正しく動作しますか?
  4. スキップリンクの確認 - ユーザーは繰り返しナビゲーションをバイパスできますか?
  5. フォームインタラクションのテスト - すべてのフォームコントロールがキーボードで動作しますか?

ブラウザーテストツール

キーボードナビゲーションの問題を特定するために、これらのツールを使用してください:

  • axe DevTools - 自動化アクセシビリティテスト
  • WAVE - Webアクセシビリティ評価
  • Lighthouse - Chrome組み込みアクセシビリティ監査

自動化テストの統合

テストスイートにキーボードナビゲーションテストを追加してください:

// Testing Libraryを使用した例
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('モーダルが正しくフォーカスをトラップする', async () => {
  const user = userEvent.setup();
  render(<ModalComponent />);
  
  const openButton = screen.getByText('モーダルを開く');
  await user.click(openButton);
  
  const modal = screen.getByRole('dialog');
  const firstButton = screen.getByText('最初のボタン');
  const lastButton = screen.getByText('最後のボタン');
  
  // フォーカスは最初の要素にあるべき
  expect(firstButton).toHaveFocus();
  
  // 最後の要素にタブして、トラップを確認
  await user.tab();
  expect(lastButton).toHaveFocus();
  
  await user.tab();
  expect(firstButton).toHaveFocus(); // 最初に戻るべき
});

複雑なコンポーネントの処理

ドロップダウンメニューとコンボボックス

カスタムドロップダウンに適切なキーボードナビゲーションを実装してください:

class AccessibleDropdown {
  constructor(element) {
    this.dropdown = element;
    this.trigger = element.querySelector('.dropdown-trigger');
    this.menu = element.querySelector('.dropdown-menu');
    this.options = Array.from(element.querySelectorAll('.dropdown-option'));
    this.currentIndex = -1;
    
    this.bindEvents();
  }

  bindEvents() {
    this.trigger.addEventListener('keydown', (e) => {
      switch(e.key) {
        case 'Enter':
        case ' ':
        case 'ArrowDown':
          e.preventDefault();
          this.open();
          break;
      }
    });

    this.menu.addEventListener('keydown', (e) => {
      switch(e.key) {
        case 'ArrowDown':
          e.preventDefault();
          this.focusNext();
          break;
        case 'ArrowUp':
          e.preventDefault();
          this.focusPrevious();
          break;
        case 'Enter':
          this.selectCurrent();
          break;
        case 'Escape':
          this.close();
          break;
      }
    });
  }

  focusNext() {
    this.currentIndex = (this.currentIndex + 1) % this.options.length;
    this.options[this.currentIndex].focus();
  }

  focusPrevious() {
    this.currentIndex = this.currentIndex <= 0 
      ? this.options.length - 1 
      : this.currentIndex - 1;
    this.options[this.currentIndex].focus();
  }
}

キーボードナビゲーション付きデータテーブル

大きなデータテーブルには効率的なキーボードナビゲーションパターンが必要です:

// テーブルナビゲーション用のローミングtabindex
class AccessibleTable {
  constructor(table) {
    this.table = table;
    this.cells = Array.from(table.querySelectorAll('td, th'));
    this.currentCell = null;
    this.setupRovingTabindex();
  }

  setupRovingTabindex() {
    this.cells.forEach(cell => {
      cell.setAttribute('tabindex', '-1');
      cell.addEventListener('keydown', this.handleKeyDown.bind(this));
    });
    
    // 最初のセルが初期フォーカスを取得
    if (this.cells[0]) {
      this.cells[0].setAttribute('tabindex', '0');
      this.currentCell = this.cells[0];
    }
  }

  handleKeyDown(e) {
    const { key } = e;
    let newCell = null;

    switch(key) {
      case 'ArrowRight':
        newCell = this.getNextCell();
        break;
      case 'ArrowLeft':
        newCell = this.getPreviousCell();
        break;
      case 'ArrowDown':
        newCell = this.getCellBelow();
        break;
      case 'ArrowUp':
        newCell = this.getCellAbove();
        break;
    }

    if (newCell) {
      e.preventDefault();
      this.moveFocus(newCell);
    }
  }

  moveFocus(newCell) {
    this.currentCell.setAttribute('tabindex', '-1');
    newCell.setAttribute('tabindex', '0');
    newCell.focus();
    this.currentCell = newCell;
  }
}

まとめ

効果的なキーボードナビゲーションアクセシビリティには、フォーカス管理、セマンティックHTMLの使用、適切なテストへの注意が必要です。論理的なDOM構造から始め、フォーカスインジケーターを保持し、0より大きいtabindex値を避け、モーダル用のフォーカストラップを実装してください。実際のキーボードナビゲーションによる定期的なテストは、自動化ツールが見逃す可能性のある問題を明らかにします。

Webアプリケーションのキーボードナビゲーションアクセシビリティを改善する準備はできていますか? Tabキーで現在のインターフェースを監査し、フォーカス管理の問題を特定し、このガイドで概説されたパターンを実装することから始めてください。ユーザーは、より包括的な体験を作成してくれることに感謝するでしょう。

よくある質問

:focusと:focus-visible CSS疑似クラスの違いは何ですか?

:focus疑似クラスは、要素がフォーカスを受け取る際に、フォーカスの方法(マウス、キーボード、またはプログラム的)に関係なく適用されます。:focus-visible疑似クラスは、ブラウザーがフォーカスを表示すべきと判断した場合のみ適用され、通常はキーボードでナビゲートする際です。これにより、必要な時のみフォーカスインジケーターを表示でき、キーボードユーザーのアクセシビリティを維持しながらマウスユーザーの体験を向上させます。

異なるブラウザー間でキーボードナビゲーションをテストするにはどうすればよいですか?

Chrome、Firefox、Safari、Edgeでインターフェースをタブで移動する手動テストを使用してください。各ブラウザーはフォーカスの処理が異なる場合があります。自動化テストには、axe DevTools、WAVE、Lighthouseなどのツールを使用してください。フォーカスインジケーターは、ブラウザー間で大きく異なるため、特に注意を払ってください。一貫したクロスブラウザーフォーカススタイリングには:focus-visibleの使用を検討してください。

CSSレイアウトが論理的なタブ順序を壊す場合はどうすればよいですか?

HTMLを視覚的フローに合わせて再構築し、CSS GridやFlexboxを使用して配置を制御してください。タブ順序の問題を修正するために正のtabindex値を使用することは避けてください。これはより多くの問題を作成します。要素を視覚的に並べ替えるためにCSSを使用する必要がある場合は、DOM順序がキーボードとスクリーンリーダーユーザーにとって論理的に意味を持つことを確認してください。

シングルページアプリケーションでキーボードナビゲーションをどのように処理しますか?

ルートが変更された際に、メインコンテンツエリアまたはページ見出しにフォーカスを移動してフォーカスを管理してください。フォーカス管理ライブラリを使用するか、カスタムフォーカス復元を実装してください。動的コンテンツの更新がタブシーケンスを壊さないこと、新しく追加されたインタラクティブ要素が適切にフォーカス可能であることを確認してください。ルート変更間でフォーカス状態を追跡するフォーカス管理システムの使用を検討してください。

カスタムコンポーネントがキーボードユーザーにアクセシブルでないのはなぜですか?

divやspan要素で構築されたカスタムコンポーネントには、ネイティブキーボードサポートがありません。フォーカス可能にするためにtabindex='0'を追加し、Enter、Space、矢印キーのキーボードイベントハンドラーを実装し、適切なARIA属性があることを確認してください。セマンティックHTML要素はデフォルトでキーボードアクセシビリティを提供するため、常に最初に検討してください。

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers

We use cookies to improve your experience. By using our site, you accept cookies.