Webアクセシビリティ実践:ARIAロールとWCAG 2.2準拠ガイド

前端工程(更新: 2026年6月17日)

WCAG 2.2の4原則

WCAG 2.2はPOURの4原則に基づいて準拠要件を整理しています:

原則 英語 中核要件
知覚可能 Perceivable すべてのユーザーが情報を知覚できる 画像alt、字幕、コントラスト
操作可能 Operable 複数の方法で操作可能 キーボードナビ、十分な時間、点滅なし
理解可能 Understandable 内容と操作が理解できる 一貫したナビ、エラー提示、入力ヘルプ
堅牢 Robust 各種支援技術で解析可能 有効なHTML、ARIAセマンティクス

ARIAロール体系

文書構造ロール

<header role="banner">
  <nav role="navigation" aria-label="メインナビゲーション">
    <ul role="menubar">
      <li role="menuitem"><a href="/home">ホーム</a></li>
      <li role="menuitem" aria-haspopup="true">
        製品
        <ul role="menu">
          <li role="menuitem"><a href="/saas">SaaS</a></li>
          <li role="menuitem"><a href="/api">API</a></li>
        </ul>
      </li>
    </ul>
  </nav>
</header>
<main role="main">
  <article role="article">
    <h2>記事タイトル</h2>
    <p>本文内容</p>
  </article>
</main>
<footer role="contentinfo">著作権情報</footer>

ウィジェットロール

ロール 用途 必須プロパティ 主要な操作
button クリック可能なトリガー なし Enter/Spaceでアクティブ化
checkbox チェック/未チェック aria-checked Spaceで切替
radio 単一選択肢 aria-checked 矢印キーで切替
slider 範囲値 aria-valuenow, aria-valuemin, aria-valuemax 矢印キーで増減
tablist / tab タブパネル aria-selected 矢印キーで切替
dialog モーダル/非モーダル aria-modal Escで閉じる
combobox 選択可能な入力 aria-expanded 上下選択/入力フィルタ
tree / treeitem ツリー構造 aria-expanded 矢印キーで展開折りたたみ

ARIAプロパティとステート

コアプロパティクイックリファレンス

<!-- ラベルと説明 -->
<button aria-label="ダイアログを閉じる">✕</button>
<input aria-labelledby="name-label" aria-describedby="name-hint">
<span id="name-label">ユーザー名</span>
<span id="name-hint">6-20文字</span>

<!-- ステート -->
<div aria-expanded="true" aria-selected="false" aria-checked="mixed">
<div aria-hidden="true" aria-disabled="true" aria-busy="true">

<!-- 関係 -->
<div aria-owns="child-id" aria-controls="panel-id" aria-flowto="next-section">

<!-- ライブ領域 -->
<div role="alert" aria-live="assertive">エラーメッセージ</div>
<div aria-live="polite" aria-atomic="true" aria-relevant="additions text">ステータス更新</div>

第一ルール:ARIAの濫用を避ける

ネイティブHTML要素が必要なセマンティクスと操作を既に提供している場合、ARIAで再実装しない。

<!-- ❌ アンチパターン -->
<div role="button" tabindex="0" onclick="submit()">送信</div>

<!-- ✅ 正しい方法 -->
<button type="submit">送信</button>

実践:アクセシブルなモーダル

<div class="modal-overlay" id="modalOverlay" hidden>
  <div
    role="dialog"
    aria-modal="true"
    aria-labelledby="modalTitle"
    aria-describedby="modalDesc"
    class="modal"
  >
    <h2 id="modalTitle">削除の確認</h2>
    <p id="modalDesc">この操作は取り消せません。本当にこのレコードを削除しますか?</p>
    <div class="modal-actions">
      <button class="btn-cancel">キャンセル</button>
      <button class="btn-confirm">削除を確認</button>
    </div>
  </div>
</div>
class AccessibleModal {
  constructor(dialogEl) {
    this.dialog = dialogEl;
    this.previousFocus = null;
  }

  open() {
    this.previousFocus = document.activeElement;
    this.dialog.hidden = false;
    const firstFocusable = this.dialog.querySelector(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    firstFocusable?.focus();
    document.addEventListener('keydown', this.handleKeydown);
  }

  close() {
    this.dialog.hidden = true;
    this.previousFocus?.focus();
    document.removeEventListener('keydown', this.handleKeydown);
  }

  handleKeydown = (e) => {
    if (e.key === 'Escape') this.close();
    if (e.key === 'Tab') this.trapFocus(e);
  };

  trapFocus(e) {
    const focusable = [...this.dialog.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    )];
    const first = focusable[0];
    const last = focusable[focusable.length - 1];
    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  }
}

実践:アクセシブルなタブ

<div class="tabs">
  <div role="tablist" aria-label="アカウント設定">
    <button
      role="tab"
      id="tab-profile"
      aria-selected="true"
      aria-controls="panel-profile"
      tabindex="0"
    >プロフィール</button>
    <button
      role="tab"
      id="tab-security"
      aria-selected="false"
      aria-controls="panel-security"
      tabindex="-1"
    >セキュリティ</button>
    <button
      role="tab"
      id="tab-notify"
      aria-selected="false"
      aria-controls="panel-notify"
      tabindex="-1"
    >通知設定</button>
  </div>

  <div role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">
    <p>プロフィール設定内容</p>
  </div>
  <div role="tabpanel" id="panel-security" aria-labelledby="tab-security" hidden>
    <p>セキュリティ設定内容</p>
  </div>
  <div role="tabpanel" id="panel-notify" aria-labelledby="tab-notify" hidden>
    <p>通知設定内容</p>
  </div>
</div>
document.querySelector('[role="tablist"]').addEventListener('keydown', (e) => {
  const tabs = [...e.currentTarget.querySelectorAll('[role="tab"]')];
  const idx = tabs.indexOf(document.activeElement);
  let nextIdx;

  if (e.key === 'ArrowRight') nextIdx = (idx + 1) % tabs.length;
  else if (e.key === 'ArrowLeft') nextIdx = (idx - 1 + tabs.length) % tabs.length;
  else if (e.key === 'Home') nextIdx = 0;
  else if (e.key === 'End') nextIdx = tabs.length - 1;
  else return;

  e.preventDefault();
  tabs.forEach((t) => { t.setAttribute('aria-selected', 'false'); t.tabIndex = -1; });
  tabs[nextIdx].setAttribute('aria-selected', 'true');
  tabs[nextIdx].tabIndex = 0;
  tabs[nextIdx].focus();
});

実践:アクセシブルなフォーム

<form aria-label="ユーザー登録">
  <div class="field">
    <label for="email">メールアドレス <span aria-hidden="true">*</span></label>
    <input
      id="email"
      type="email"
      required
      aria-required="true"
      autocomplete="email"
      aria-invalid="false"
      aria-describedby="email-error"
    >
    <span id="email-error" role="alert" aria-live="assertive"></span>
  </div>

  <div class="field">
    <label for="password">パスワード</label>
    <input
      id="password"
      type="password"
      minlength="8"
      aria-describedby="pwd-rules pwd-strength"
    >
    <span id="pwd-rules">8文字以上、大文字・小文字・数字を含む</span>
    <span id="pwd-strength" aria-live="polite"></span>
  </div>

  <fieldset>
    <legend>通知方法</legend>
    <label><input type="checkbox" name="notify" value="email"> メール</label>
    <label><input type="checkbox" name="notify" value="sms"> SMS</label>
  </fieldset>

  <button type="submit">登録</button>
</form>

自動検出ツール比較

ツール タイプ WCAGカバレッジ 統合方法 特徴
axe-core ランタイムライブラリ A/AA CI + ブラウザ 最も包括的、誤検知なし
Lighthouse 監査ツール A CLI / DevTools パフォーマンス+アクセシビリティ
WAVE ブラウザ拡張 A/AA 手動 視覚的アノテーション
NVDA スクリーンリーダー 手動 実ユーザー体験
Playwright a11y E2Eテスト A CI テストフローと統合
import { test } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('アクセシビリティチェック', async ({ page }) => {
  await page.goto('/register');
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
    .analyze();
  expect(results.violations).toEqual([]);
});

WCAG 2.2の新基準

新基準 レベル 説明
2.4.11 Focus Appearance AAA フォーカスインジケータ面積 ≥ 2px、コントラスト ≥ 3:1
2.4.12 Focus Not Obscured AAA フォーカス要素が他のコンテンツに隠れない
3.2.6 Consistent Help A ヘルプメカニズムの位置がページ間で一貫
3.3.7 Redundant Entry A 同一プロセスで既に提供した情報の再入力を要求しない
3.3.8 Accessible Authentication AA 認証が認知機能テストに依存しない

ベストプラクティスまとめ

  1. ネイティブ要素を優先<button><input>などでARIAの再実装を避ける
  2. フォーカス管理:モーダル/ルート切替後に論理的な位置にフォーカス
  3. ライブ領域は慎重にaria-live="assertive"は重要な通知のみに使用
  4. 自動 + 手動:axe-coreは約30-40%の問題をカバー、スクリーンリーダーの手動テストは不可欠
  5. 継続的インテグレーション:CIパイプラインにアクセシビリティチェックを組み込み、リグレッションを防止

ブラウザローカルツールを無料で試す →

#无障碍#ARIA#WCAG#屏幕阅读器#可访问性