Webアクセシビリティ完全ガイド2026 - WCAG 2.2対応の実践テクニック


はじめに

Webアクセシビリティ(a11y: accessibility)は、すべてのユーザーがWebサイトやアプリケーションを利用できるようにする取り組みです。

2026年現在、アクセシビリティは単なる「あったら良い機能」ではなく、ビジネスとしての必須要件になっています。

  • 法的義務: 欧米では障害者差別禁止法により義務化
  • SEO効果: セマンティックHTMLは検索エンジンにも有利
  • UX向上: アクセシブルなサイトはすべてのユーザーにとって使いやすい
  • 市場拡大: 世界人口の約15%が何らかの障害を持つ

この記事では、WCAG 2.2(Web Content Accessibility Guidelines 2.2)に準拠した実装方法を、実践的なコード例とともに解説します。

WCAG 2.2とは?

WCAG(Web Content Accessibility Guidelines)は、W3Cが定めるWebアクセシビリティの国際標準です。

バージョン履歴

  • WCAG 2.0(2008年): 基礎となる標準
  • WCAG 2.1(2018年): モバイル・ロービジョン対応強化
  • WCAG 2.2(2023年): 認知・学習障害への配慮追加

4つの原則(POUR)

  1. Perceivable(知覚可能): 情報とUIが知覚できる
  2. Operable(操作可能): UIとナビゲーションが操作できる
  3. Understandable(理解可能): 情報とUIが理解できる
  4. Robust(堅牢): 様々な技術で解釈できる

適合レベル

  • Level A: 最低限の基準(必須)
  • Level AA: 推奨レベル(多くの法規制が要求)
  • Level AAA: 最高レベル(一部のコンテンツで実現困難)

一般的な目標: Level AA準拠

セマンティックHTML

アクセシビリティの基礎は、適切なHTML要素の使用です。

悪い例(非セマンティック)

<div class="header">
  <div class="nav">
    <div class="nav-item" onclick="navigate('/home')">ホーム</div>
    <div class="nav-item" onclick="navigate('/about')">会社概要</div>
  </div>
</div>

<div class="main">
  <div class="article">
    <div class="title">記事タイトル</div>
    <div class="content">本文...</div>
  </div>
</div>

<div class="footer">
  <div>© 2026 Company</div>
</div>

問題点:

  • スクリーンリーダーが構造を理解できない
  • キーボード操作不可(onclickのdiv)
  • SEOに不利

良い例(セマンティック)

<header>
  <nav aria-label="メインナビゲーション">
    <ul>
      <li><a href="/home">ホーム</a></li>
      <li><a href="/about">会社概要</a></li>
    </ul>
  </nav>
</header>

<main>
  <article>
    <h1>記事タイトル</h1>
    <p>本文...</p>
  </article>
</main>

<footer>
  <p><small>© 2026 Company</small></p>
</footer>

改善点:

  • <header>, <nav>, <main>, <article>, <footer>でランドマーク明確化
  • <a>要素でキーボード操作可能
  • <h1>で見出し階層を明示

主要なセマンティック要素

ランドマーク要素

<header>    <!-- サイト/セクションのヘッダー -->
<nav>       <!-- ナビゲーション -->
<main>      <!-- メインコンテンツ(ページに1つ) -->
<article>   <!-- 独立したコンテンツ(ブログ記事等) -->
<section>   <!-- セクション(見出し付き) -->
<aside>     <!-- サイドバー・補足情報 -->
<footer>    <!-- フッター -->

見出し階層

<h1>サイトタイトル / ページタイトル</h1>
  <h2>セクション1</h2>
    <h3>サブセクション1-1</h3>
    <h3>サブセクション1-2</h3>
  <h2>セクション2</h2>
    <h3>サブセクション2-1</h3>

重要: h1→h2→h3と順番に使用(h1→h3は×)

フォーム要素

<form>
  <div>
    <label for="username">ユーザー名</label>
    <input
      type="text"
      id="username"
      name="username"
      required
      aria-describedby="username-help"
    >
    <small id="username-help">半角英数字4文字以上</small>
  </div>

  <fieldset>
    <legend>通知設定</legend>
    <label>
      <input type="checkbox" name="email-notify" checked>
      メール通知
    </label>
    <label>
      <input type="checkbox" name="sms-notify">
      SMS通知
    </label>
  </fieldset>

  <button type="submit">送信</button>
</form>

ポイント:

  • <label><input>for/idで関連付け
  • <fieldset><legend>でグループ化
  • aria-describedbyでヘルプテキスト関連付け

ARIA(Accessible Rich Internet Applications)

ARIAは、HTMLだけでは表現できないアクセシビリティ情報を補完します。

ARIA 5つのルール

  1. 可能な限りネイティブHTMLを使う(ARIAは最後の手段)
  2. ネイティブの意味を変えない<button role="heading">は×)
  3. すべてのインタラクティブ要素はキーボード操作可能にする
  4. role="presentation"aria-hidden="true"を使う要素にフォーカスさせない
  5. すべてのインタラクティブ要素にアクセシブルな名前をつける

主要なARIA属性

ロール(role)

<!-- ナビゲーション -->
<div role="navigation" aria-label="グローバルナビゲーション">
  <!-- <nav>の方が推奨 -->
</div>

<!-- タブパネル -->
<div role="tablist">
  <button role="tab" aria-selected="true" aria-controls="panel-1">
    タブ1
  </button>
  <button role="tab" aria-selected="false" aria-controls="panel-2">
    タブ2
  </button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
  パネル1の内容
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
  パネル2の内容
</div>

<!-- アラート -->
<div role="alert" aria-live="assertive">
  エラー: 入力内容に誤りがあります
</div>

ステート(aria-*)

<!-- チェック状態 -->
<button aria-pressed="false" onclick="toggleButton(this)">
  通知オフ
</button>

<!-- 展開/折りたたみ -->
<button
  aria-expanded="false"
  aria-controls="menu"
  onclick="toggleMenu()"
>
  メニュー
</button>
<ul id="menu" hidden>
  <li><a href="/profile">プロフィール</a></li>
  <li><a href="/settings">設定</a></li>
</ul>

<!-- ローディング状態 -->
<button aria-busy="true" disabled>
  <span aria-hidden="true">⏳</span>
  読み込み中...
</button>

<!-- 無効状態 -->
<button aria-disabled="true" disabled>
  送信
</button>

プロパティ(aria-*)

<!-- ラベル -->
<button aria-label="閉じる">
  <span aria-hidden="true">×</span>
</button>

<!-- 説明 -->
<input
  type="password"
  aria-describedby="password-requirements"
>
<div id="password-requirements">
  8文字以上、大文字・小文字・数字を含む
</div>

<!-- ライブリージョン -->
<div aria-live="polite" aria-atomic="true">
  <p>検索結果: 42件</p>
</div>

実践例: アクセシブルなモーダルダイアログ

<!-- トリガーボタン -->
<button onclick="openModal()">
  ダイアログを開く
</button>

<!-- モーダルダイアログ -->
<div
  id="modal"
  role="dialog"
  aria-modal="true"
  aria-labelledby="modal-title"
  aria-describedby="modal-description"
  hidden
>
  <div class="modal-overlay" onclick="closeModal()"></div>
  <div class="modal-content">
    <h2 id="modal-title">確認</h2>
    <p id="modal-description">
      この操作を実行してもよろしいですか?
    </p>
    <button onclick="confirm()">はい</button>
    <button onclick="closeModal()">キャンセル</button>
    <button
      aria-label="閉じる"
      class="close-btn"
      onclick="closeModal()"
    >
      ×
    </button>
  </div>
</div>

<script>
let previousFocus;

function openModal() {
  const modal = document.getElementById('modal');
  previousFocus = document.activeElement;

  modal.hidden = false;

  // フォーカストラップ
  const focusableElements = modal.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const firstElement = focusableElements[0];
  const lastElement = focusableElements[focusableElements.length - 1];

  firstElement.focus();

  modal.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
      closeModal();
    }
    if (e.key === 'Tab') {
      if (e.shiftKey && document.activeElement === firstElement) {
        e.preventDefault();
        lastElement.focus();
      } else if (!e.shiftKey && document.activeElement === lastElement) {
        e.preventDefault();
        firstElement.focus();
      }
    }
  });

  // 背景のスクロールを防ぐ
  document.body.style.overflow = 'hidden';
}

function closeModal() {
  const modal = document.getElementById('modal');
  modal.hidden = true;
  document.body.style.overflow = '';

  // 元の要素にフォーカスを戻す
  if (previousFocus) {
    previousFocus.focus();
  }
}
</script>

ポイント:

  • role="dialog"aria-modal="true"
  • aria-labelledbyでタイトルを関連付け
  • フォーカストラップ(Tab/Shift+Tabで循環)
  • Escキーで閉じる
  • 閉じたら元の要素にフォーカスを戻す

キーボードナビゲーション

すべての機能がマウスなしで操作可能である必要があります。

基本ルール

  • Tab: 次の要素へ移動
  • Shift+Tab: 前の要素へ移動
  • Enter/Space: ボタン/リンクの実行
  • 矢印キー: カスタムウィジェット内の移動
  • Esc: ダイアログ/メニューを閉じる

タブインデックス

<!-- 通常のタブ順序(自動) -->
<input type="text">  <!-- tabindex=0(暗黙) -->
<button>送信</button>  <!-- tabindex=0(暗黙) -->

<!-- カスタムタブ順序(非推奨、DOM順を優先すべき) -->
<input tabindex="1">
<input tabindex="2">

<!-- フォーカス可能にする(慎重に使用) -->
<div tabindex="0" role="button" onclick="...">
  カスタムボタン
</div>

<!-- フォーカス不可にする -->
<div tabindex="-1">
  プログラム的にのみフォーカス可能
</div>

重要: tabindex > 0は避ける(タブ順序が予測不可能になる)

フォーカススタイル

/* デフォルトのアウトラインを消さない */
button:focus {
  outline: none; /* ❌ 絶対ダメ */
}

/* カスタムフォーカススタイルを提供 */
button:focus-visible {
  outline: 3px solid #4A90E2;
  outline-offset: 2px;
}

/* マウスクリック時は非表示(:focus-visibleの利点) */
button:focus:not(:focus-visible) {
  outline: none;
}

スキップリンク

<body>
  <a href="#main-content" class="skip-link">
    メインコンテンツへスキップ
  </a>
  <header>...</header>
  <nav>...</nav>
  <main id="main-content">
    <!-- メインコンテンツ -->
  </main>
</body>

<style>
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: #000;
  color: #fff;
  padding: 8px;
  text-decoration: none;
  z-index: 100;
}

.skip-link:focus {
  top: 0;
}
</style>

スクリーンリーダー対応

視覚障害者が使用するスクリーンリーダー(NVDA、JAWS、VoiceOver等)への対応。

代替テキスト

画像

<!-- 情報を含む画像 -->
<img
  src="chart.png"
  alt="2026年1月の売上グラフ。前年比120%の成長"
>

<!-- 装飾画像 -->
<img src="decoration.png" alt="" role="presentation">

<!-- リンク内の画像 -->
<a href="/profile">
  <img src="avatar.png" alt="ユーザープロフィール">
</a>

<!-- 複雑な情報 -->
<figure>
  <img src="complex-chart.png" alt="年間売上推移">
  <figcaption>
    <details>
      <summary>詳細データ</summary>
      <table>
        <!-- 詳細なデータテーブル -->
      </table>
    </details>
  </figcaption>
</figure>

アイコンフォント

<!-- ❌ 悪い例 -->
<button>
  <i class="icon-trash"></i>
</button>

<!-- ✅ 良い例 -->
<button aria-label="削除">
  <i class="icon-trash" aria-hidden="true"></i>
</button>

<!-- ✅ より良い例(テキストも表示) -->
<button>
  <i class="icon-trash" aria-hidden="true"></i>
  <span>削除</span>
</button>

SVG

<svg role="img" aria-labelledby="logo-title">
  <title id="logo-title">会社ロゴ</title>
  <path d="..."></path>
</svg>

<!-- 装飾SVG -->
<svg aria-hidden="true" focusable="false">
  <path d="..."></path>
</svg>

非表示コンテンツ

/* スクリーンリーダー専用テキスト(視覚的に非表示) */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

/* フォーカス時に表示 */
.sr-only-focusable:focus {
  position: static;
  width: auto;
  height: auto;
  overflow: visible;
  clip: auto;
  white-space: normal;
}
<button>
  <span aria-hidden="true">→</span>
  <span class="sr-only">次へ</span>
</button>

ライブリージョン

動的に更新されるコンテンツをスクリーンリーダーに通知。

<!-- 重要度: 低(polite) -->
<div aria-live="polite">
  検索結果を更新中...
</div>

<!-- 重要度: 高(assertive) -->
<div role="alert" aria-live="assertive">
  エラー: ネットワーク接続が切れました
</div>

<!-- Reactでの実装例 -->
<script>
function SearchResults({ results, loading }) {
  return (
    <div>
      <div aria-live="polite" aria-atomic="true">
        {loading ? (
          <p>検索中...</p>
        ) : (
          <p>{results.length}件の結果が見つかりました</p>
        )}
      </div>
      <ul>
        {results.map(result => (
          <li key={result.id}>{result.title}</li>
        ))}
      </ul>
    </div>
  );
}
</script>

色とコントラスト

WCAG 2.2 コントラスト比要件

  • Level AA:

    • 通常テキスト(18pt未満): 4.5:1以上
    • 大きいテキスト(18pt以上 or 太字14pt以上): 3:1以上
    • UI要素・グラフィック: 3:1以上
  • Level AAA:

    • 通常テキスト: 7:1以上
    • 大きいテキスト: 4.5:1以上

色だけに頼らない

<!-- ❌ 悪い例(色のみで区別) -->
<p style="color: red;">エラー</p>
<p style="color: green;">成功</p>

<!-- ✅ 良い例(アイコン + テキスト + 色) -->
<div class="error">
  <span role="img" aria-label="エラー">❌</span>
  <strong>エラー:</strong> 入力内容に誤りがあります
</div>

<div class="success">
  <span role="img" aria-label="成功">✅</span>
  <strong>成功:</strong> 保存しました
</div>

実例: アクセシブルなカラーパレット

:root {
  /* WCAG AA準拠(白背景) */
  --text-primary: #212121;      /* 16.1:1 */
  --text-secondary: #757575;    /* 4.6:1 */
  --link: #1976D2;              /* 4.5:1 */
  --error: #D32F2F;             /* 4.5:1 */
  --success: #388E3C;           /* 4.5:1 */

  /* ダークモード */
  --dark-bg: #121212;
  --dark-text: #E0E0E0;         /* 12.6:1 */
  --dark-link: #90CAF9;         /* 8.6:1 */
}

body {
  background: white;
  color: var(--text-primary);
}

@media (prefers-color-scheme: dark) {
  body {
    background: var(--dark-bg);
    color: var(--dark-text);
  }
}

テストツール

自動テストツール

1. axe DevTools(Chrome拡張)

# npm版(CI/CD統合)
npm install -D @axe-core/cli

# テスト実行
npx axe https://example.com

2. Lighthouse

# Chrome DevTools > Lighthouse > Accessibility

# CLI
npm install -g lighthouse
lighthouse https://example.com --only-categories=accessibility

3. WAVE(Web Accessibility Evaluation Tool)

https://wave.webaim.org/

4. Pa11y

npm install -g pa11y

pa11y https://example.com

手動テスト

キーボードテスト

  1. Tabキーですべての要素にアクセスできるか
  2. フォーカス順序が論理的か
  3. フォーカスインジケーターが見えるか
  4. Enterキー/Spaceキーで操作できるか
  5. Escキーでダイアログを閉じられるか

スクリーンリーダーテスト

  • Windows: NVDA(無料)
  • Mac: VoiceOver(標準搭載)
  • iOS: VoiceOver
  • Android: TalkBack
# VoiceOver(Mac)
Command + F5

# 基本操作
VO + →: 次の要素
VO + ←: 前の要素
VO + Space: 実行
VO + U: ローター(見出し・リンク一覧)

React向けテストライブラリ

npm install -D @testing-library/react @testing-library/jest-dom
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('ボタンがアクセシブル', async () => {
  render(<button aria-label="メニューを開く">☰</button>);

  const button = screen.getByRole('button', { name: /メニューを開く/i });
  expect(button).toBeInTheDocument();

  await userEvent.click(button);
  // ...
});

test('フォームがアクセシブル', () => {
  render(<LoginForm />);

  const usernameInput = screen.getByLabelText(/ユーザー名/i);
  const passwordInput = screen.getByLabelText(/パスワード/i);

  expect(usernameInput).toBeRequired();
  expect(passwordInput).toHaveAttribute('type', 'password');
});

フレームワーク別ベストプラクティス

React

// ❌ 悪い例
function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;

  return (
    <div className="modal" onClick={onClose}>
      <div className="modal-content">
        {children}
        <button onClick={onClose}>×</button>
      </div>
    </div>
  );
}

// ✅ 良い例
import { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';

function Modal({ isOpen, onClose, title, children }) {
  const previousFocusRef = useRef(null);
  const modalRef = useRef(null);

  useEffect(() => {
    if (isOpen) {
      previousFocusRef.current = document.activeElement;
      modalRef.current?.focus();
      document.body.style.overflow = 'hidden';
    } else {
      previousFocusRef.current?.focus();
      document.body.style.overflow = '';
    }
  }, [isOpen]);

  useEffect(() => {
    const handleEscape = (e) => {
      if (e.key === 'Escape' && isOpen) {
        onClose();
      }
    };

    document.addEventListener('keydown', handleEscape);
    return () => document.removeEventListener('keydown', handleEscape);
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return createPortal(
    <div
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
      ref={modalRef}
      tabIndex={-1}
      className="modal"
    >
      <div
        className="modal-overlay"
        onClick={onClose}
        aria-hidden="true"
      />
      <div className="modal-content">
        <h2 id="modal-title">{title}</h2>
        {children}
        <button onClick={onClose} aria-label="閉じる">
          ×
        </button>
      </div>
    </div>,
    document.body
  );
}

Next.js

import Head from 'next/head';

export default function Page() {
  return (
    <>
      <Head>
        <title>ページタイトル - サイト名</title>
        <meta name="description" content="ページ説明" />
        <html lang="ja" />
      </Head>

      <a href="#main" className="skip-link">
        メインコンテンツへ
      </a>

      <header>
        <nav aria-label="メインナビゲーション">
          {/* ... */}
        </nav>
      </header>

      <main id="main">
        {/* コンテンツ */}
      </main>
    </>
  );
}

まとめ

アクセシビリティチェックリスト

HTML/セマンティック

  • セマンティックHTML要素を使用
  • 見出し階層が正しい(h1→h2→h3)
  • lang属性を設定

フォーム

  • すべての<input><label>
  • エラーメッセージが明確
  • required属性と視覚的表示の一致

キーボード

  • すべての機能がキーボードで操作可能
  • フォーカスインジケーターが見える
  • タブ順序が論理的
  • スキップリンクを提供

スクリーンリーダー

  • すべての画像に適切なalt
  • アイコンにaria-label
  • ライブリージョンで動的更新を通知

色/コントラスト

  • コントラスト比4.5:1以上(AA)
  • 色だけに頼らない情報提示

テスト

  • axe DevToolsでエラーゼロ
  • キーボード操作テスト完了
  • スクリーンリーダーテスト完了

参考リソース

アクセシビリティは一度に完璧にする必要はありません。小さな改善を積み重ねることが大切です。すべてのユーザーにとって使いやすいWebを一緒に作りましょう!