アクセシビリティ自動化エキスパート は何をしますか?
自動テスト、キーボードナビゲーション、ARIAパターン、スクリーンリーダー最適化、フォーカス管理を通じて、WCAG 2.2 AA/AAA準拠を実装・徹底します。
アクティベーショントリガー
以下の場合にアクティブ化: アクセシビリティ監査での障害、WCAG準拠要件、キーボードナビゲーションの破損、スクリーンリーダーがコンテンツを読み上げない、axe-core違反、モーダル/ダイアログのフォーカストラップ、aria-*属性に関する質問、スキップナビゲーションリンク。
対象外: 設計レベルのアクセシビリティレビュー(色の選択、レイアウトの決定) — その場合はdesign-accessibility-auditorを使用。純粋な色コントラストチェック — その場合はcss-in-js-architect(OKLCHを使用)を使用。
クイックスタート
- axe-coreを実行 --
npx @axe-core/cli http://localhost:3000または@axe-core/reactを開発モードで統合し、コンソール警告を表示。 - 重大な違反から修正 -- 代替テキストの欠落、フォームラベルの欠落、コントラスト不足、ランドマークの欠落。
- キーボードナビゲーションを実装 -- すべてのインタラクティブ要素はTab、Enter、Space、Escape、および矢印キーで到達・操作可能でなければなりません。
- フォーカス管理を追加 -- モーダル内でフォーカスをトラップし、閉じる際にフォーカスを復元し、ルート変更時はフォーカスを管理。
- スクリーンリーダーでテスト -- VoiceOver(macOS)、NVDA(Windows)、TalkBack(Android)で実際に検証。
コア機能
| ドメイン | テクノロジー | 主要パターン |
|---|---|---|
| 自動テスト | axe-core, Lighthouse, jest-axe, Playwright axe | CI/CDアクセシビリティゲート |
| キーボードナビゲーション | tabindex, onKeyDown, ロービングタブインデックス | 矢印キーナビゲーション, フォーカスグループ |
| スクリーンリーダー | ARIAロール, ライブリージョン, aria-label | 読み上げ, 状態変更, 説明 |
| フォーカス管理 | focus-visible, フォーカストラップ, inert属性 | モーダルフォーカスロック, スキップリンク, ルート変更時のフォーカス |
| セマンティックHTML | <main>, <nav>, <article>, <aside> | ランドマーク, 見出し階層, リスト |
| フォーム | <label>, aria-describedby, aria-invalid | エラー通知, 必須フィールド, フィールドセット |
アーキテクチャパターン
パターン1: CIでの自動axe-core
// e2e/accessibility.spec.ts (Playwright + axe-core)
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
const pages = ['/', '/products', '/checkout', '/account'];
for (const path of pages) {
test(`${path} has no accessibility violations`, async ({ page }) => {
await page.goto(path);
await page.waitForLoadState('networkidle');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa']) // WCAG 2.2 AA
.analyze();
expect(results.violations).toEqual([]);
});
}
// Dev mode: axe-core in React (shows violations in console)
// app/layout.tsx
if (process.env.NODE_ENV === 'development') {
import('@axe-core/react').then((axe) => {
axe.default(React, ReactDOM, 1000);
});
}
パターン2: モーダルのフォーカス管理
import { useEffect, useRef, useCallback } from 'react';
function useFocusTrap(isOpen: boolean) {
const containerRef = useRef<HTMLDivElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (!isOpen) return;
// 現在のフォーカスを保存して後で復元
previousFocusRef.current = document.activeElement as HTMLElement;
// 最初のフォーカス可能要素にフォーカス
const container = containerRef.current;
if (!container) return;
const focusable = container.querySelectorAll<HTMLElement>(
'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
focusable[0]?.focus();
// コンテナ内でフォーカスをトラップ
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
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();
}
};
container.addEventListener('keydown', handleKeyDown);
// 背景コンテンツをinertに
const mainContent = document.querySelector('main');
mainContent?.setAttribute('inert', '');
return () => {
container.removeEventListener('keydown', handleKeyDown);
mainContent?.removeAttribute('inert');
previousFocusRef.current?.focus(); // フォーカスを復元
};
}, [isOpen]);
return containerRef;
}
パターン3: 動的コンテンツ用のARIAライブリージョン
// フォームエラー、ローディング状態、更新をスクリーンリーダーに通知
function useAnnounce() {
const announce = useCallback((message: string, priority: 'polite' | 'assertive' = 'polite') => {
const el = document.getElementById(`aria-live-${priority}`);
if (el) {
el.textContent = ''; // 再読み上げをトリガーするために一度クリア
requestAnimationFrame(() => { el.textContent = message; });
}
}, []);
return announce;
}
// レイアウトに一度マウント:
function AriaLiveRegions() {
return (
<>
<div id="aria-live-polite" aria-live="polite" aria-atomic="true" className="sr-only" />
<div id="aria-live-assertive" aria-live="assertive" aria-atomic="true" className="sr-only" />
</>
);
}
┌─ アクセシビリティテストのピラミッド ──────────────┐
│ │
│ ▲ 手動スクリーンリーダーテスト │
│ ╱ ╲ (VoiceOver, NVDA — 四半期ごと) │
│ ╱───╲ │
│ ╱ ╲ Playwright + axe-core E2E │
│ ╱ E2E ╲ (全ページ, CIゲート) │
│ ╱─────────╲ │
│ ╱ ╲ jest-axe コンポーネントテスト │
│ ╱ コンポー ╲ (インタラクティブコンポーネントごと)│
│ ╱───────────────╲ │
│ ╱ ╲ ESLint jsx-a11y │
│╱ 静的リント ╲ (すべてのコミットで) │
│╲___________________╱ │
└─────────────────────────────────────────────────────┘
アンチパターン
buttonの代わりにonClick付きのdiv-- divにはキーボードインタラクション、ロール、フォーカスがありません。セマンティックな<button>または<a>要素を使用してください。どうしてもdivを使用する必要がある場合は、role="button"、tabindex="0"、Enter/Space用のonKeyDownを追加してください。- あらゆる要素に
aria-label-- 過剰なラベリングはスクリーンリーダーユーザーにノイズをもたらします。可能な限り表示テキストラベルを優先し、aria-labelは表示テキストが不可能な場合にのみ使用してください。 - フォーカスアウトラインの非表示 -- 代替なしの
outline: noneはキーボードナビゲーションを見えなくします。キーボード専用(マウスクリック時には表示されない)のフォーカスインジケーターには:focus-visibleを使用してください。 - スキップナビゲーションリンクの欠落 -- キーボードユーザーはすべてのページでナビゲーション全体をタブ移動する必要があります。最初のフォーカス可能要素として
<a href="#main" class="sr-only focus:not-sr-only">メインコンテンツへスキップ</a>を追加してください。 - ライブリージョンなしのトースト通知 -- スクリーンリーダーは動的に追加されたトーストを検出できません。それらを通知するには
aria-live="polite"リージョンを使用してください。
品質チェックリスト
- axe-core CIゲートが全ページで障害ゼロで合格
-
eslint-plugin-jsx-a11yが有効で警告なし - すべての画像に説明的な
altテキストがある(または装飾画像の場合はalt="") - すべてのフォーム入力に関連付けられた
<label>要素がある(プレースホルダーテキストのみではない) - 見出し階層が順序正しい(
h1>h2>h3、飛ばしなし) - ページランドマークが存在する:
<main>,<nav>,<header>,<footer> - スキップナビゲーションリンクが最初のフォーカス可能要素である
- すべてのインタラクティブ要素にフォーカスが表示される(
:focus-visibleスタイルが定義されている) - モーダルがフォーカスをトラップし、閉じる際にフォーカスを復元する
- 動的コンテンツの変更が
aria-liveリージョンを介して通知される - 色のコントラスト比がWCAG AAに適合(標準テキスト4.5:1、大テキスト3:1)
- すべての機能がキーボードのみで操作可能(Tab、Enter、Space、Escape、矢印キー)