Webアクセシビリティ実装ガイド2026|WCAG準拠・WAI-ARIA・テスト自動化


なぜアクセシビリティが重要なのか

Webアクセシビリティとは、障害の有無に関わらずすべての人がWebコンテンツを利用できるようにすることです。

2026年現在、アクセシビリティは「あると良い」ではなくビジネス要件になりつつあります。

  • 法的義務: EU(European Accessibility Act 2025年施行)、日本(障害者差別解消法)
  • SEO効果: セマンティックHTMLはGoogleのクローリング精度を向上
  • ユーザー拡大: 世界人口の15%(約10億人)が何らかの障害を持つ
  • UX向上: アクセシビリティ改善はすべてのユーザーの体験を改善

WCAG 2.2 の4原則

WCAG(Web Content Accessibility Guidelines)は4つの原則に基づいています:

1. 知覚可能(Perceivable)

コンテンツをユーザーが知覚できること。

<!-- ❌ 画像にalt属性がない -->
<img src="chart.png">

<!-- ✅ 適切な代替テキスト -->
<img src="chart.png" alt="2026年Q1の売上推移グラフ。1月100万円、2月120万円、3月150万円">

<!-- ✅ 装飾画像は空のalt -->
<img src="decorative-border.png" alt="">

動画のアクセシビリティ

<video controls>
  <source src="tutorial.mp4" type="video/mp4">
  <!-- 字幕トラック -->
  <track kind="captions" src="captions-ja.vtt" srclang="ja" label="日本語" default>
  <!-- 音声解説 -->
  <track kind="descriptions" src="descriptions-ja.vtt" srclang="ja" label="音声解説">
</video>

色だけに依存しない情報伝達

/* ❌ 色だけでエラーを表現 */
.error { color: red; }

/* ✅ 色 + アイコン + テキストで表現 */
.error {
  color: #d32f2f;
  border-left: 4px solid #d32f2f;
  padding-left: 12px;
}
.error::before {
  content: "⚠ ";
}

2. 操作可能(Operable)

すべてのUIをキーボードで操作できること。

// ❌ クリックのみ対応
<div onClick={handleAction}>アクション</div>

// ✅ キーボードアクセシブル
<button onClick={handleAction}>アクション</button>

// ✅ カスタム要素をキーボード対応にする場合
<div
  role="button"
  tabIndex={0}
  onClick={handleAction}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      handleAction();
    }
  }}
>
  アクション
</div>

フォーカス管理

/* ❌ フォーカスインジケータを消す */
*:focus { outline: none; }

/* ✅ 見やすいフォーカススタイル */
*:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
  border-radius: 2px;
}

スキップリンク

<!-- ページ最上部に配置 -->
<a href="#main-content" class="skip-link">
  メインコンテンツにスキップ
</a>

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

3. 理解可能(Understandable)

コンテンツが理解できること。

<!-- 言語の指定 -->
<html lang="ja">

<!-- 部分的に異なる言語 -->
<p>これは<span lang="en">accessibility</span>に関する記事です。</p>

エラーメッセージ

// ✅ 具体的なエラーメッセージ
<div role="alert">
  <p>入力内容に問題があります:</p>
  <ul>
    <li>メールアドレスの形式が正しくありません(例: user@example.com)</li>
    <li>パスワードは8文字以上必要です(現在5文字)</li>
  </ul>
</div>

4. 堅牢(Robust)

さまざまな支援技術で解釈できること。

<!-- ✅ セマンティックHTML -->
<header>
  <nav aria-label="メインナビゲーション">...</nav>
</header>
<main>
  <article>
    <h1>記事タイトル</h1>
    <section aria-labelledby="section-1">
      <h2 id="section-1">セクション1</h2>
    </section>
  </article>
  <aside aria-label="関連記事">...</aside>
</main>
<footer>...</footer>

WAI-ARIA 実践パターン

ライブリージョン(動的コンテンツの通知)

// トースト通知
function Toast({ message }: { message: string }) {
  return (
    <div
      role="alert"
      aria-live="assertive"
      aria-atomic="true"
    >
      {message}
    </div>
  );
}

// 検索結果の件数通知
function SearchResults({ count }: { count: number }) {
  return (
    <div aria-live="polite" aria-atomic="true">
      {count}件の結果が見つかりました
    </div>
  );
}

モーダルダイアログ

function Modal({ isOpen, onClose, title, children }) {
  const dialogRef = useRef<HTMLDialogElement>(null);

  useEffect(() => {
    if (isOpen) {
      dialogRef.current?.showModal();
    } else {
      dialogRef.current?.close();
    }
  }, [isOpen]);

  return (
    <dialog
      ref={dialogRef}
      aria-labelledby="modal-title"
      onClose={onClose}
    >
      <div role="document">
        <h2 id="modal-title">{title}</h2>
        {children}
        <button onClick={onClose} aria-label="閉じる">

        </button>
      </div>
    </dialog>
  );
}

タブUI

function Tabs({ tabs }: { tabs: { label: string; content: ReactNode }[] }) {
  const [activeIndex, setActiveIndex] = useState(0);

  const handleKeyDown = (e: KeyboardEvent, index: number) => {
    switch (e.key) {
      case 'ArrowRight':
        setActiveIndex((index + 1) % tabs.length);
        break;
      case 'ArrowLeft':
        setActiveIndex((index - 1 + tabs.length) % tabs.length);
        break;
      case 'Home':
        setActiveIndex(0);
        break;
      case 'End':
        setActiveIndex(tabs.length - 1);
        break;
    }
  };

  return (
    <div>
      <div role="tablist" aria-label="コンテンツタブ">
        {tabs.map((tab, i) => (
          <button
            key={i}
            role="tab"
            id={`tab-${i}`}
            aria-selected={activeIndex === i}
            aria-controls={`panel-${i}`}
            tabIndex={activeIndex === i ? 0 : -1}
            onClick={() => setActiveIndex(i)}
            onKeyDown={(e) => handleKeyDown(e, i)}
          >
            {tab.label}
          </button>
        ))}
      </div>
      {tabs.map((tab, i) => (
        <div
          key={i}
          role="tabpanel"
          id={`panel-${i}`}
          aria-labelledby={`tab-${i}`}
          hidden={activeIndex !== i}
        >
          {tab.content}
        </div>
      ))}
    </div>
  );
}

アコーディオン

function Accordion({ items }: { items: { title: string; content: string }[] }) {
  const [openIndex, setOpenIndex] = useState<number | null>(null);

  return (
    <div>
      {items.map((item, i) => (
        <div key={i}>
          <h3>
            <button
              aria-expanded={openIndex === i}
              aria-controls={`accordion-panel-${i}`}
              onClick={() => setOpenIndex(openIndex === i ? null : i)}
            >
              {item.title}
              <span aria-hidden="true">{openIndex === i ? '▲' : '▼'}</span>
            </button>
          </h3>
          <div
            id={`accordion-panel-${i}`}
            role="region"
            aria-labelledby={`accordion-header-${i}`}
            hidden={openIndex !== i}
          >
            <p>{item.content}</p>
          </div>
        </div>
      ))}
    </div>
  );
}

フォームのアクセシビリティ

基本パターン

<!-- ✅ ラベルとinputの関連付け -->
<div>
  <label for="email">メールアドレス<span aria-hidden="true">*</span></label>
  <input
    id="email"
    type="email"
    name="email"
    required
    aria-required="true"
    aria-describedby="email-hint email-error"
    aria-invalid="true"
  >
  <p id="email-hint" class="hint">例: user@example.com</p>
  <p id="email-error" class="error" role="alert">
    メールアドレスの形式が正しくありません
  </p>
</div>

グループ化

<fieldset>
  <legend>配送方法を選択してください</legend>
  <div>
    <input type="radio" id="shipping-standard" name="shipping" value="standard">
    <label for="shipping-standard">通常配送(3〜5営業日)</label>
  </div>
  <div>
    <input type="radio" id="shipping-express" name="shipping" value="express">
    <label for="shipping-express">速達(翌営業日)</label>
  </div>
</fieldset>

テスト自動化

axe-core + Playwright

// e2e/accessibility.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('アクセシビリティテスト', () => {
  test('トップページにWCAG違反がないこと', async ({ page }) => {
    await page.goto('/');

    const results = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
      .analyze();

    expect(results.violations).toEqual([]);
  });

  test('フォームページのアクセシビリティ', async ({ page }) => {
    await page.goto('/contact');

    const results = await new AxeBuilder({ page })
      .include('form')
      .analyze();

    expect(results.violations).toEqual([]);
  });
});

eslint-plugin-jsx-a11y

// .eslintrc.json
{
  "extends": ["plugin:jsx-a11y/recommended"],
  "rules": {
    "jsx-a11y/alt-text": "error",
    "jsx-a11y/anchor-is-valid": "error",
    "jsx-a11y/no-autofocus": "warn",
    "jsx-a11y/label-has-associated-control": "error"
  }
}

Storybook + a11yアドオン

// .storybook/main.ts
export default {
  addons: ['@storybook/addon-a11y'],
};

チェックリスト

【基本】
☐ すべての画像にalt属性がある
☐ 見出しが順序通り(h1→h2→h3)
☐ ページにlang属性が設定されている
☐ セマンティックHTML(header/main/nav/footer)

【操作】
☐ すべての機能がキーボードで操作可能
☐ フォーカスインジケータが見える
☐ スキップリンクが実装されている
☐ モーダルにフォーカストラップがある

【フォーム】
☐ すべてのinputにlabelが関連付けられている
☐ エラーメッセージが具体的
☐ 必須項目が明示されている

【色・コントラスト】
☐ テキストのコントラスト比 4.5:1以上
☐ 大きなテキストのコントラスト比 3:1以上
☐ 色だけに依存していない

【テスト】
☐ axe-coreで自動テスト
☐ キーボードのみで全ページ操作確認
☐ スクリーンリーダーでの確認(VoiceOver/NVDA)

アクセシビリティは「対応すべき追加作業」ではなく、良いHTMLを書くことの自然な結果です。セマンティックHTMLを基本にし、WAI-ARIAで補完し、自動テストで品質を維持する——このアプローチで、すべてのユーザーに使いやすいWebを作りましょう。