WCAG 2.2 アクセシビリティ
深い知識:包括的な WCAG ガイドライン、適合基準、達成方法については、テクノロジーに wcag を指定して mcp__documentation__fetch_docs を使用してください。
このスキルを使用しない場合
- 自動テストのセットアップ - axe テストツールの統合には
axe-core スキルを使用してください
- コンポーネントライブラリのアクセシビリティ - アクセシブルなコンポーネントパターンについては、React や Vue などのフレームワーク固有のスキルを使用してください
- デザインシステム - 既製のアクセシブルなコンポーネントについては、UI ライブラリのスキルを使用してください
- ARIA の実装のみ - このスキルは、ARIA だけでなく、より広範な WCAG 準拠を扱います
公式リファレンス
適合レベル
| レベル | 説明 | 法的要件 |
|---|
| A | 最低限のアクセシビリティ | 十分とされることはまれ |
| AA | 標準的なアクセシビリティ | ほとんどの規制(ADA、EN 301 549) |
| AAA | 強化されたアクセシビリティ | 特殊な状況 |
WCAG 2.2 達成基準の数
| レベル | 2.2 での新規項目 | 合計 |
|---|
| A | 0 | 30 |
| AA | 6 | 24 |
| AAA | 3 | 31 |
POUR の原則
1. 知覚可能
| ガイドライン | 主な基準 | レベル |
|---|
| 1.1 テキストによる代替 | すべての非テキストコンテンツにテキストによる代替がある | A |
| 1.2 時間依存メディア | キャプション、音声解説 | A-AAA |
| 1.3 適応可能 | コンテンツの構造、意味のある順序 | A |
| 1.4 判別可能 | 色のコントラスト、テキストの拡大、間隔 | A-AAA |
// Text alternatives
<img src="chart.png" alt="Q3 sales increased 25% compared to Q2" />
// Decorative images
<img src="divider.png" alt="" role="presentation" />
// Color contrast (4.5:1 for normal text, 3:1 for large text)
// Use tools: WebAIM Contrast Checker, axe DevTools
2. 操作可能
| ガイドライン | 主な基準 | レベル |
|---|
| 2.1 キーボード | すべての機能をキーボードで操作できる | A |
| 2.2 十分な時間 | 時間制限を調整できる | A-AAA |
| 2.4 ナビゲーション可能 | スキップリンク、フォーカス順序、フォーカスの可視化 | A-AAA |
| 2.5 入力モダリティ | ターゲットサイズ、動作の代替手段 | A-AAA |
// Skip link
<a href="#main-content" className="skip-link">
Skip to main content
</a>
// Focus visible (2.4.7)
button:focus {
outline: 3px solid #005fcc;
outline-offset: 2px;
}
// Target size minimum (2.5.8) - 24x24 CSS pixels
.button {
min-width: 44px;
min-height: 44px;
padding: 12px 16px;
}
3. 理解可能
| ガイドライン | 主な基準 | レベル |
|---|
| 3.1 読みやすい | ページの言語、一般的でない単語 | A-AAA |
| 3.2 予測可能 | 一貫したナビゲーション、識別 | A-AA |
| 3.3 入力支援 | エラーの特定、ラベル | A-AAA |
// Language of page (3.1.1)
<html lang="en">
// Error identification (3.3.1)
<div role="alert" aria-live="assertive">
Email is required and must be valid
</div>
// Labels (3.3.2)
<label htmlFor="email">Email Address</label>
<input id="email" type="email" aria-describedby="email-hint" />
<span id="email-hint">We'll never share your email</span>
4. 堅牢
| ガイドライン | 主な基準 | レベル |
|---|
| 4.1 互換性 | 有効な HTML、名前/役割/値 | A |
// Name, Role, Value (4.1.2)
<button aria-pressed="true" aria-label="Favorite this item">
★
</button>
// Custom controls
<div
role="slider"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={50}
aria-label="Volume"
tabIndex={0}
/>
WCAG 2.2 の新規項目
レベル AA(必須)
| 基準 | 説明 |
|---|
| 2.4.11 フォーカスが隠されない(最低限) | フォーカスされた要素が少なくとも部分的に見える |
| 2.4.13 フォーカスの外観 | フォーカスインジケーターがサイズとコントラストの要件を満たす |
| 2.5.7 ドラッグ操作 | ドラッグに代わる単一ポインター操作を用意する |
| 2.5.8 ターゲットサイズ(最低限) | 最低 24x24 CSS ピクセル |
| 3.2.6 一貫したヘルプ | 一貫した場所にヘルプを表示する |
| 3.3.7 冗長な入力 | 情報の再入力を求めない |
レベル AAA
| 基準 | 説明 |
|---|
| 2.4.12 フォーカスが隠されない(強化) | フォーカスされた要素が完全に見える |
| 3.3.8 アクセシブルな認証(最低限) | 認知機能テストを行わない |
| 3.3.9 アクセシブルな認証(強化) | オブジェクトやコンテンツの認識を求めない |
一般的なパターン
モーダルダイアログ
function Modal({ isOpen, onClose, title, children }) {
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isOpen) {
// Trap focus inside modal
modalRef.current?.focus();
// Prevent body scroll
document.body.style.overflow = 'hidden';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
ref={modalRef}
tabIndex={-1}
onKeyDown={(e) => e.key === 'Escape' && onClose()}
>
<h2 id="modal-title">{title}</h2>
{children}
<button onClick={onClose}>Close</button>
</div>
);
}
ドロップダウンメニュー
function Dropdown({ label, items }) {
const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
const handleKeyDown = (e: KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setActiveIndex(i => Math.min(i + 1, items.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setActiveIndex(i => Math.max(i - 1, 0));
break;
case 'Escape':
setIsOpen(false);
break;
case 'Enter':
case ' ':
if (activeIndex >= 0) items[activeIndex].onClick();
break;
}
};
return (
<div onKeyDown={handleKeyDown}>
<button
aria-haspopup="menu"
aria-expanded={isOpen}
onClick={() => setIsOpen(!isOpen)}
>
{label}
</button>
{isOpen && (
<ul role="menu">
{items.map((item, i) => (
<li
key={item.id}
role="menuitem"
tabIndex={activeIndex === i ? 0 : -1}
aria-current={activeIndex === i}
>
{item.label}
</li>
))}
</ul>
)}
</div>
);
}
バリデーション付きフォーム
function Form() {
const [errors, setErrors] = useState<Record<string, string>>({});
return (
<form aria-describedby="form-errors">
{Object.keys(errors).length > 0 && (
<div id="form-errors" role="alert" aria-live="polite">
<h2>Please fix the following errors:</h2>
<ul>
{Object.entries(errors).map(([field, msg]) => (
<li key={field}>
<a href={`#${field}`}>{msg}</a>
</li>
))}
</ul>
</div>
)}
<div>
<label htmlFor="email">
Email <span aria-hidden="true">*</span>
<span className="sr-only">(required)</span>
</label>
<input
id="email"
type="email"
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<span id="email-error" role="alert">{errors.email}</span>
)}
</div>
</form>
);
}
テスト
axe-core による自動テスト
// Playwright + axe
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('homepage has no accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});
// Vitest + axe
import { axe, toHaveNoViolations } from 'jest-axe';
import { render } from '@testing-library/react';
expect.extend(toHaveNoViolations);
test('Button is accessible', async () => {
const { container } = render(<Button>Click me</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
手動テストのチェックリスト
## Keyboard Navigation
- [ ] Tab through all interactive elements
- [ ] Shift+Tab navigates backwards
- [ ] Enter/Space activates buttons and links
- [ ] Arrow keys work in menus, tabs, sliders
- [ ] Escape closes modals and dropdowns
- [ ] No keyboard traps
## Screen Reader
- [ ] All images have alt text
- [ ] Form fields have labels
- [ ] Headings are hierarchical (h1 > h2 > h3)
- [ ] Links are descriptive (not "click here")
- [ ] Dynamic content announced (aria-live)
## Visual
- [ ] Color contrast meets 4.5:1 (normal text)
- [ ] Color contrast meets 3:1 (large text, UI components)
- [ ] Focus indicators visible
- [ ] Content readable at 200% zoom
- [ ] No horizontal scrolling at 320px width
ツール
| ツール | 目的 |
|---|
| axe DevTools | 監査用のブラウザー拡張機能 |
| WAVE | 視覚的なアクセシビリティ評価 |
| Lighthouse | パフォーマンスとアクセシビリティの監査 |
| NVDA/VoiceOver | スクリーンリーダーのテスト |
| Color Contrast Analyzer | コントラストのチェック |
CSS ユーティリティ
/* Visually hidden but accessible to screen readers */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Focus visible only for keyboard users */
:focus:not(:focus-visible) {
outline: none;
}
:focus-visible {
outline: 3px solid #005fcc;
outline-offset: 2px;
}
/* Reduced motion preference */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* High contrast mode support */
@media (forced-colors: active) {
.button {
border: 2px solid currentColor;
}
}
チェックリスト
設計フェーズ
開発
テスト
アンチパターン
| アンチパターン | 問題点 | 正しい方法 |
|---|
role のない <div> で aria-label を使用する | ロールのない非セマンティック要素上の ARIA は無視されます | まずセマンティック HTML を使用します。<div role="button"> の代わりに <button> を使用します |
ボタンに <div onclick=""> を使用する | デフォルトではキーボードでアクセスできません | 適切なイベントハンドラーを設定した <button> を使用します |
| 色だけのインジケーター | 色覚異常のあるユーザーには機能しません | 色とともにアイコン、テキスト、またはパターンを追加します |
ラベルの代わりに placeholder を使用する | 入力時に消え、コントラストも低くなります | 常に for 属性を持つ <label> を使用します |
tabindex > 0 | 自然なタブ順序が乱れます | tabindex="0" を使用するか、DOM の順序に従います |
情報を伝える画像に alt="" を使用する | スクリーンリーダーが重要なコンテンツを読み飛ばします | 説明的な alt テキストを指定します |
| フォーカスインジケーターがない | ユーザーが現在の位置を確認できません | 常に :focus-visible スタイルを表示します |
| メディアの自動再生 | スクリーンリーダーのユーザーを混乱させます | ユーザーの操作でメディアを開始するようにします |
クイックトラブルシューティング
| 問題 | 原因 | 解決策 |
|---|
| スクリーンリーダーがボタンを読み上げない | アクセシブルな名前がありません | aria-label を追加するか、ボタン内に表示テキストを配置します |
| モーダルでキーボードトラップが発生する | フォーカスが適切に管理されていません | 最初と最後の要素を使ったフォーカストラップを実装します |
| フォームのエラーが読み上げられない | role="alert" または aria-live がありません | エラーコンテナーに aria-live="assertive" を追加します |
| コントラストが低いという警告が出る | テキストが 4.5:1 の比率を満たしていません | コントラストチェッカーを使用し、色を調整するかフォントサイズを大きくします |
| タブ順序がおかしい | DOM の順序が視覚的な順序と一致していません | DOM の順序を視覚的なレイアウトに合わせ、tabindex > 0 は避けます |
| カスタムドロップダウンがアクセシブルでない | ARIA ロールやキーボード操作が不足しています | role="combobox"、aria-expanded、矢印キーによるナビゲーションを使用します |
| 動的コンテンツが読み上げられない | 変更が通知されずに発生しています | aria-live="polite" または role="status" を使用します |
| 画像ボタンにラベルがない | アイコンのみで、テキストによる代替がありません | 説明的なテキストを含む aria-label を追加します |
関連スキル