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 | 認証が認知機能テストに依存しない |
ベストプラクティスまとめ
- ネイティブ要素を優先:
<button>、<input>などでARIAの再実装を避ける - フォーカス管理:モーダル/ルート切替後に論理的な位置にフォーカス
- ライブ領域は慎重に:
aria-live="assertive"は重要な通知のみに使用 - 自動 + 手動:axe-coreは約30-40%の問題をカバー、スクリーンリーダーの手動テストは不可欠
- 継続的インテグレーション:CIパイプラインにアクセシビリティチェックを組み込み、リグレッションを防止
ブラウザローカルツールを無料で試す →
#无障碍#ARIA#WCAG#屏幕阅读器#可访问性