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)
- Perceivable(知覚可能): 情報とUIが知覚できる
- Operable(操作可能): UIとナビゲーションが操作できる
- Understandable(理解可能): 情報とUIが理解できる
- 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つのルール
- 可能な限りネイティブHTMLを使う(ARIAは最後の手段)
- ネイティブの意味を変えない(
<button role="heading">は×) - すべてのインタラクティブ要素はキーボード操作可能にする
role="presentation"やaria-hidden="true"を使う要素にフォーカスさせない- すべてのインタラクティブ要素にアクセシブルな名前をつける
主要な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)
4. Pa11y
npm install -g pa11y
pa11y https://example.com
手動テスト
キーボードテスト
- Tabキーですべての要素にアクセスできるか
- フォーカス順序が論理的か
- フォーカスインジケーターが見えるか
- Enterキー/Spaceキーで操作できるか
- 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でエラーゼロ
- キーボード操作テスト完了
- スクリーンリーダーテスト完了
参考リソース
- WCAG 2.2: https://www.w3.org/WAI/WCAG22/quickref/
- MDN Web Docs - Accessibility: https://developer.mozilla.org/ja/docs/Web/Accessibility
- The A11Y Project: https://www.a11yproject.com/
- WebAIM: https://webaim.org/
アクセシビリティは一度に完璧にする必要はありません。小さな改善を積み重ねることが大切です。すべてのユーザーにとって使いやすいWebを一緒に作りましょう!