- 技術 💻
WCAG 2.2チェックリスト|A・AA全55項目と確認方法・axe検査手順
WCAG 2.2 のチェックリストとは、W3C のウェブアクセシビリティ指針 WCAG 2.2(2023年10月5日勧告、2024年12月12日更新版)の達成基準を、レベル A・AA の 55 項目(廃止された 4.1.1 を含めると 56 行)について「何を・どう確認し・どう直すか」まで落とし込んだ一覧です。 各項目に「自動・手動・両方」の確認区分、axe-core 4.13.0 の対応ルール名、NG → OK の HTML を付けました。
さらに、違反を意図的に含めたサンプルページを Playwright 1.56.1 + @axe-core/playwright 4.13.0 で検査した実行出力、修正後の再検査で 0 件になるまでの流れ、Tab キーでフォーカス順を記録した結果、コントラスト比を計算する Python の出力を載せています。すべて実際に動かした結果です。
WCAG 2.2 チェックリストの早見表
| 知りたいこと | 答え |
|---|---|
| 最新版 | WCAG 2.2(2023年10月5日 W3C 勧告、2024年12月12日更新版)。日本語訳は WAIC(ウェブアクセシビリティ基盤委員会)が更新版をもとに公開 |
| 達成基準の数 | 全 86 項目(A 31 / AA 24 / AAA 31)。一般的な目標は A + AA の 55 項目(廃止の 4.1.1 を含めると 56 行) |
| 2.2 で追加 | 9 項目。A:3.2.6 一貫したヘルプ、3.3.7 冗長な入力項目/AA:2.4.11 隠されないフォーカス(最低限)、2.5.7 ドラッグ動作、2.5.8 ターゲットのサイズ(最低限)、3.3.8 アクセシブルな認証(最低限)/AAA:2.4.12、2.4.13、3.3.9 |
| 2.2 で廃止 | 4.1.1 構文解析。HTML では常に満たすとみなされ、確認不要 |
| 自動で見られる範囲 | 本記事の区分では 55 項目中 20 項目に axe-core のルールがあり、35 項目は手動確認が必須。Deque の調査(課題の件数ベース)では自動テストで約 57% を検出 |
| 自動検査ツール | axe-core 4.13.0(105 ルール。うち best-practice 30、experimental 7)。Playwright からは @axe-core/playwright、ブラウザでは axe DevTools、Lighthouse の Accessibility 監査も内部は axe-core |
| 手動確認の基本 5 つ | キーボードだけで操作/200% と 400% の拡大/幅 320px のリフロー/コントラスト比/スクリーンリーダー |
| コントラスト比の基準 | 通常テキスト 4.5:1(AA)、大きなテキストと UI 部品・図形 3:1、AAA は 7:1 |
| JIS との関係 | JIS X 8341-3:2016 の達成基準は WCAG 2.0 と同一。2.2 AA を満たせば JIS の AA 相当も満たす |
| 法律との関係 | 2024年4月1日から事業者も「合理的配慮の提供」が義務。ウェブアクセシビリティ対応は「環境の整備」(努力義務)で、WCAG・JIS 準拠そのものが義務化されたわけではない |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「WCAG 2.2チェックリスト|A・AA全55項目と確認方法・axe検査手順」
WCAG 2.2 A・AA 全項目チェックリスト(確認方法・axe ルール・NG → OK)
「確認」列は axe-core 4.13.0 のルールで検出できるかを基準に、自動(ルールでほぼ判定できる)、両方(一部を検出し、残りは目視)、手動(ルールがない)に分けています。 「exp」は experimental(既定で実行されない実験的ルール)、「既定無効」は axe.run() の既定では動かないルール(タグやルール名で明示すると動く。後述)です。名称は WAIC の WCAG 2.2 日本語訳に合わせました。
1. 知覚可能(20 項目)
| 番号 | 名称 | Lv | 確認 | axe-core のルール例 | NG → OK の HTML |
|---|---|---|---|---|---|
| 1.1.1 | 非テキストコンテンツ | A | 両方 | image-alt, svg-img-alt | <img src="a.png"> → <img src="a.png" alt="備品管理">(装飾は alt="") |
| 1.2.1 | 音声だけ及び映像だけ(収録済み) | A | 手動 | audio-caption(既定無効) | 音声だけ → 書き起こしテキストを併記 |
| 1.2.2 | キャプション(収録済み) | A | 両方 | video-caption | <video> に <track kind="captions" srclang="ja"> |
| 1.2.3 | 音声解説、又はメディアに対する代替(収録済み) | A | 手動 | — | 映像に音声解説か同内容のテキスト |
| 1.2.4 | キャプション(ライブ) | AA | 手動 | — | ライブ配信にリアルタイム字幕 |
| 1.2.5 | 音声解説(収録済み) | AA | 手動 | — | 収録済み映像に音声解説トラック |
| 1.3.1 | 情報及び関係性 | A | 両方 | list, th-has-data-cells, aria-required-children | <div class="h2"> → <h2>、表の見出しは <th scope="col"> |
| 1.3.2 | 意味をなすシーケンス | A | 手動 | — | CSS の order で読み順がずれる → DOM 順を読み順に |
| 1.3.3 | 感覚による特性 | A | 手動 | — | 「右の赤いボタン」→「右の『送信』ボタン」 |
| 1.3.4 | 向き(orientation) | AA | 手動 | css-orientation-lock(exp) | 縦向き固定 → 縦横どちらでも使える |
| 1.3.5 | 入力目的の特定 | AA | 両方 | autocomplete-valid | autocomplete="mail-address" → autocomplete="email" |
| 1.4.1 | 色の用途 | A | 両方 | link-in-text-block | 必須を赤色だけで示す → 「必須」の文字を添える |
| 1.4.2 | 音声の制御 | A | 両方 | no-autoplay-audio | <audio autoplay> → 自動再生をやめる/停止ボタン |
| 1.4.3 | コントラスト(最低限) | AA | 両方 | color-contrast | color:#999(2.85:1)→ color:#595959(7:1) |
| 1.4.4 | テキストのサイズ変更 | AA | 両方 | meta-viewport | user-scalable=no を削除し、200% でも読める |
| 1.4.5 | 文字画像 | AA | 手動 | — | 画像の見出し → CSS で装飾した実テキスト |
| 1.4.10 | リフロー | AA | 手動 | — | 幅 320px で横スクロール → max-width:100%、flex-wrap:wrap |
| 1.4.11 | 非テキストのコントラスト | AA | 手動 | — | 入力欄の枠 #ddd → #767676(3:1 以上) |
| 1.4.12 | テキストの間隔 | AA | 両方 | avoid-inline-spacing | line-height:1 !important → !important を外す |
| 1.4.13 | ホバー時又はフォーカス時のコンテンツ | AA | 手動 | — | 消せないツールチップ → Esc で閉じられる |
2. 操作可能(20 項目)
| 番号 | 名称 | Lv | 確認 | axe-core のルール例 | NG → OK の HTML |
|---|---|---|---|---|---|
| 2.1.1 | キーボード | A | 両方 | scrollable-region-focusable | <div onclick>送信</div> → <button type="submit">送信</button> |
| 2.1.2 | キーボードトラップなし | A | 手動 | — | モーダルから抜けられない → Esc で閉じ、元の要素に戻す |
| 2.1.4 | 文字キーによるショートカット | A | 手動 | — | 「s」単独で発動 → 無効化か修飾キー併用に変更できる |
| 2.2.1 | 期限調節可能 | A | 両方 | meta-refresh | <meta http-equiv="refresh"> → 自動更新をやめる/延長できる |
| 2.2.2 | 一時停止・停止・非表示 | A | 両方 | blink, marquee | 自動再生カルーセル → 一時停止ボタン |
| 2.3.1 | 3 回の閃光、又は閾値以下 | A | 手動 | — | 1 秒に 3 回を超える点滅 → 点滅をなくす |
| 2.4.1 | ブロックのバイパス | A | 両方 | bypass | 先頭に <a href="#main">本文へスキップ</a>、本文を <main id="main"> |
| 2.4.2 | ページタイトル | A | 両方 | document-title | <title> なし → <title>備品の予約 | 備品管理</title> |
| 2.4.3 | フォーカス順序 | A | 手動 | tabindex(best-practice) | tabindex="5" → 削除し、DOM 順を見た目の順に |
| 2.4.4 | リンクの目的(文脈内) | A | 両方 | link-name | <a>こちら</a> → <a>備品一覧を見る</a> |
| 2.4.5 | 複数の経路 | AA | 手動 | — | メニューだけ → サイト内検索かサイトマップを追加 |
| 2.4.6 | 見出し及びラベル | AA | 手動 | empty-heading(best-practice) | 空の <h2> → 内容を表す見出し文 |
| 2.4.7 | フォーカスの可視化 | AA | 手動 | — | :focus{outline:none} → :focus-visible{outline:2px solid #0a5fd0} |
| 2.4.11 | 隠されないフォーカス(最低限) | AA | 手動 | — | 固定ヘッダーが覆う → html{scroll-padding-top:80px} |
| 2.5.1 | ポインタジェスチャ | A | 手動 | — | ピンチ操作のみ → +/-ボタンも用意 |
| 2.5.2 | ポインタキャンセル | A | 手動 | — | mousedown で実行 → click で実行 |
| 2.5.3 | ラベルを含む名前(name) | A | 手動 | label-content-name-mismatch(exp) | 表示「送信」+ aria-label="go" → 名前に「送信」を含める |
| 2.5.4 | 動きによる起動 | A | 手動 | — | 端末を振って取り消し → ボタンでも取り消せる |
| 2.5.7 | ドラッグ動作 | AA | 手動 | — | ドラッグのみの並べ替え → 「上へ/下へ」ボタンを併設 |
| 2.5.8 | ターゲットのサイズ(最低限) | AA | 両方 | target-size(既定無効) | 16px 角のボタン → min-width:24px; min-height:24px |
3. 理解可能(13 項目)
| 番号 | 名称 | Lv | 確認 | axe-core のルール例 | NG → OK の HTML |
|---|---|---|---|---|---|
| 3.1.1 | ページの言語 | A | 自動 | html-has-lang, html-lang-valid | <html> → <html lang="ja"> |
| 3.1.2 | 一部分の言語 | AA | 両方 | valid-lang | 英文の段落 → <p lang="en"> |
| 3.2.1 | フォーカス時 | A | 手動 | — | フォーカスだけで遷移 → 明示的な操作で遷移 |
| 3.2.2 | 入力時 | A | 手動 | — | <select onchange="submit()"> → 「表示」ボタンで送信 |
| 3.2.3 | 一貫したナビゲーション | AA | 手動 | — | ページごとにメニュー順が違う → 共通ヘッダーに統一 |
| 3.2.4 | 一貫した識別性 | AA | 手動 | — | 同じ機能に別の名前 → 同じラベルに統一 |
| 3.2.6 | 一貫したヘルプ | A | 手動 | — | 問い合わせリンクの位置が違う → 全ページ同じ位置 |
| 3.3.1 | エラーの特定 | A | 手動 | — | 枠を赤くするだけ → 誤りの内容をテキストで表示 |
| 3.3.2 | ラベル又は指示 | A | 両方 | label, select-name | placeholder のみ → <label for="item">備品名</label> |
| 3.3.3 | エラーの修正提案 | AA | 手動 | — | 「エラー」だけ → 「全角で入力してください」など直し方 |
| 3.3.4 | 誤り防止(法的・取引・データ) | AA | 手動 | — | 即確定 → 確認画面か取り消し手段 |
| 3.3.7 | 冗長な入力項目 | A | 手動 | — | 同じ住所を再入力 → 自動入力か「前と同じ」を選べる |
| 3.3.8 | アクセシブルな認証(最低限) | AA | 手動 | — | 貼り付け禁止 → 貼り付け可 + autocomplete="current-password" |
4. 堅牢(2 項目 + 廃止 1)
| 番号 | 名称 | Lv | 確認 | axe-core のルール例 | NG → OK の HTML |
|---|---|---|---|---|---|
| 4.1.1 | 構文解析(廃止及び削除) | — | — | duplicate-id(既定無効) | 2.2 では確認不要 |
| 4.1.2 | 名前(name)・役割(role)・値(value) | A | 両方 | button-name, aria-valid-attr-value, aria-roles | <button></button> → <button aria-label="次の日">></button> |
| 4.1.3 | ステータスメッセージ | AA | 手動 | — | 「保存しました」を div に挿入 → <div role="status"> に挿入 |
集計: 55 項目のうち「自動」1、「両方」19、「手動」35。自動検査で手がかりが得られるのは 20 項目(36%) で、残りは人が操作して確かめます(Deque の 57% は課題の件数ベースで、測り方が違います)。
WCAG 2.2 で追加された 9 基準の確認手順(2.1 からの差分)
WCAG 2.1 で AA 対応済みのサイトが 2.2 に上げるとき、新たに見る項目は AA までなら 6 つです。2.5.8 の target-size 以外は axe-core にルールがないため、手順を決めておくと確認が速くなります。
| 番号 | 名称 | Lv | 求めていること | 確認手順 | 実装の例 |
|---|---|---|---|---|---|
| 2.4.11 | 隠されないフォーカス(最低限) | AA | フォーカスした部品が、固定ヘッダー・固定フッター・Cookie バナーなど作者が作った要素に完全には隠れない | Tab で全要素を巡回し、固定要素の裏に入る瞬間がないか見る。長いフォームと末尾の固定バーが典型 | html { scroll-padding-top: 80px; scroll-padding-bottom: 64px; }、固定バーは閉じられるように |
| 2.4.12 | 隠されないフォーカス(高度) | AAA | 一部分も隠れない | 同上(部分的な重なりも対象) | 固定要素を減らす |
| 2.4.13 | フォーカスの外観 | AAA | フォーカス表示が「部品の外周 2px 分」以上の面積で、前後の差が 3:1 以上 | リングの太さと色を測る | :focus-visible { outline: 2px solid #0a5fd0; outline-offset: 2px; } |
| 2.5.7 | ドラッグ動作 | AA | ドラッグで行う機能は、ドラッグなしの単一ポインタ操作でもできる(本質的な場合を除く) | 並べ替え・スライダー・地図のパン・ドロップ領域を列挙し、クリックだけで同じことができるか試す | 並べ替えに「上へ/下へ」ボタン、スライダーに数値入力、ドロップ領域に「ファイルを選択」 |
| 2.5.8 | ターゲットのサイズ(最低限) | AA | クリック対象は 24×24 CSS px 以上。例外:24px の円が隣と重ならない間隔/同じ機能の別の対象が満たす/文中のインラインリンク/ブラウザ既定の部品/本質的な場合 | withTags([... 'wcag22aa']) で target-size を実行し、例外は目視 |
min-width: 24px; min-height: 24px;、アイコンボタンに padding |
| 3.2.6 | 一貫したヘルプ | A | 連絡先・問い合わせフォーム・FAQ・チャットなどのヘルプ手段が複数ページにあるなら、相対的に同じ位置・順序に置く | 3 ページ以上でヘッダー・フッターのヘルプリンクの位置を比べる | 共通フッターに「お問い合わせ」「よくある質問」を同じ順で |
| 3.3.7 | 冗長な入力項目 | A | 同じ手続きで一度入力した情報は、自動入力されるか選択できる(再入力が本質的・セキュリティ上必要・無効になった場合を除く) | 複数ステップのフォームで同じ項目を 2 回聞いていないか洗い出す | 「配送先は登録住所と同じ」チェック、確認画面で入力値を保持 |
| 3.3.8 | アクセシブルな認証(最低限) | AA | 認証のどの段階でも「記憶・転記・パズル」を要求しない。例外:別の方法/貼り付けやパスワードマネージャーで補助/物体認識/本人提供コンテンツの識別 | パスワード欄に貼り付けできるか、autocomplete が付いているか、CAPTCHA に代替手段があるか試す |
<input type="password" autocomplete="current-password">、onpaste 抑止を外す、メールのリンクやパスキーも選べる |
| 3.3.9 | アクセシブルな認証(高度) | AAA | 物体認識・本人コンテンツの例外もなし | 同上 | 画像選択型 CAPTCHA を使わない |
4.1.1 構文解析は廃止されました。2.2 に対しては確認不要で、2.0 / 2.1 への適合を報告する場合も正誤表で「HTML・XML では常に満たすとみなす」とされています。axe-core 4.13.0 でも duplicate-id は既定で無効です(id の重複が ARIA 参照を壊す問題は duplicate-id-aria が 4.1.2 の観点で検出します)。
Playwright + axe-core で自動検査する手順(実行結果つき)
自動検査の最短ルートは、Playwright で開いたページに @axe-core/playwright を当てることです。 Node 22.22 / Playwright 1.56.1 / @axe-core/playwright 4.13.0(内蔵 axe-core 4.13.0)で実行しました。
npm install playwright@1.56 @axe-core/playwright@4.13.0
# Chromium が未導入なら: npx playwright install chromium
検査対象:違反を意図的に含めたサンプル(bad.html)
python3 -m http.server 7740 で配信し、http://localhost:7740/bad.html として検査しました。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<style>
.note { color: #999999; background: #ffffff; }
.btn-small { width: 16px; height: 16px; padding: 0; border: 1px solid #ccc; background: #eee; }
a:focus, button:focus { outline: none; }
</style>
</head>
<body>
<img src="logo.png">
<a href="/"><img src="home-icon.png" alt=""></a>
<h1>備品の予約</h1>
<h3>予約フォーム</h3>
<p class="note">※ 予約は1人3件までです。</p>
<form action="/reserve" method="post">
<input type="text" placeholder="備品名">
<input type="email" placeholder="連絡先メール" autocomplete="mail-address">
<select><option>会議室A</option><option>会議室B</option></select>
<input type="checkbox" id="agree"> 利用規約に同意する
<div onclick="submitForm()" style="color:#0a5fd0; cursor:pointer;">送信</div>
</form>
<p>
<button class="btn-small" type="button"></button>
<button class="btn-small" type="button"></button>
<button class="btn-small" type="button"></button>
</p>
<div aria-labelledby="missing-id" role="region"><p>お知らせ: 今月の点検日は第2金曜日です。</p></div>
<ul>
<li><a href="/items">こちら</a></li>
<li><a href="/items/1" tabindex="5">詳細</a></li>
</ul>
</body>
</html>
検査スクリプト(scan.mjs)と実行結果
// scan.mjs — 実行: node scan.mjs http://localhost:7740/bad.html
import { chromium } from 'playwright';
import { AxeBuilder } from '@axe-core/playwright';
const browser = await chromium.launch();
const context = await browser.newContext(); // browser.newPage() 直接では動かない(後述)
const page = await context.newPage();
await page.goto(process.argv[2]);
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa']) // WCAG 2.2 AA までに絞る
.analyze();
await browser.close();
const nodes = results.violations.reduce((n, v) => n + v.nodes.length, 0);
console.log(`axe-core ${results.testEngine.version} | violations: ${results.violations.length} rules / ${nodes} nodes | incomplete: ${results.incomplete.length}`);
for (const v of results.violations) console.log(`${v.id} | ${v.impact} | ${v.nodes.length} | ${v.tags.filter(t => /^wcag\d{3,4}$/.test(t))}`);
for (const i of results.incomplete) console.log(`incomplete: ${i.id} | ${i.nodes.length}`);
$ node scan.mjs http://localhost:7740/bad.html
axe-core 4.13.0 | violations: 11 rules / 17 nodes | incomplete: 1
autocomplete-valid | serious | 1 | wcag135
button-name | critical | 3 | wcag412
color-contrast | serious | 1 | wcag143
document-title | serious | 1 | wcag242
html-has-lang | serious | 1 | wcag311
image-alt | critical | 1 | wcag111
label | critical | 1 | wcag412
link-name | serious | 1 | wcag244,wcag412
meta-viewport | moderate | 1 | wcag144
select-name | critical | 1 | wcag412
target-size | serious | 5 | wcag258
incomplete: aria-valid-attr-value | 1
つまずき: browser.newPage() でページを作ると analyze() が次のエラーで止まります。browser.newContext() → context.newPage() の順にすると解決します。
Error: Please use browser.newContext()
Please check out https://github.com/dequelabs/axe-core-npm/blob/develop/packages/playwright/error-handling.md
at AxeBuilder.analyze (.../node_modules/@axe-core/playwright/dist/index.mjs:218:13)
ノード単位のメッセージ(violations[n].nodes[m].any[0].message)には、直すための数値がそのまま入っています。
color-contrast Element has insufficient color contrast of 2.84 (foreground color: #999999, background color: #ffffff,
font size: 12.0pt (16px), font weight: normal). Expected contrast ratio of 4.5:1
target-size Target has insufficient size (16px by 16px, should be at least 24px by 24px)
incomplete ARIA attribute element ID does not exist on the page: aria-labelledby="missing-id"
この出力から、自動検査の性格が 3 つ読み取れます。
placeholder="備品名"のテキスト入力はlabelに引っかからず、チェックボックスだけが検出されました。axe は placeholder をアクセシブルな名前として受け入れるため、見える<label>を付けるかは手動で判断します(3.3.2)<div onclick>の「送信」とa:focus{outline:none}は検出されません。キーボード操作(2.1.1)とフォーカスの可視化(2.4.7)は手動確認の領域ですaria-labelledby="missing-id"は violations ではなく incomplete(判定保留)に入ります。レポートでは incomplete も必ず出力します
修正して再検査
11 ルールをチェックリスト表の NG → OK のとおりに直し(lang="ja"、<title>、user-scalable=no の削除、alt、color:#595959、<label> と autocomplete="email"、ボタンに aria-label と min-width:24px; min-height:24px、リスト内リンクに display:inline-block; padding:4px 0)、手動確認の分として <div onclick> を <button type="submit"> に、outline:none を :focus-visible のスタイルに、tabindex="5" を削除、aria-labelledby の参照先を実在する <h2 id="news-heading"> にしました。
$ node scan.mjs http://localhost:7740/good.html
axe-core 4.13.0 | violations: 0 rules / 0 nodes | incomplete: 0
既定設定で走らせたときとの違い(target-size と best-practice)
タグを指定せず new AxeBuilder({ page }).analyze() とすると結果が変わります。 同じ bad.html での比較です。
default : 14 rules -> autocomplete-valid, button-name, color-contrast, document-title, heading-order,
html-has-lang, image-alt, label, landmark-one-main, link-name, meta-viewport, region, select-name, tabindex
withRules: target-size 5 nodes
既定では best-practice ルール(heading-order、landmark-one-main、region、tabindex)が混ざり、逆に target-size は走りません。axe-core 4.13.0 で既定無効なのは target-size、duplicate-id、duplicate-id-active、audio-caption、color-contrast-enhanced、aria-roledescription、identical-links-same-purpose、landmark-complementary-is-top-level、meta-refresh-no-exceptions の 9 ルールで、withTags() か withRules(['target-size']) で明示すると実行されます。WCAG のタグを明示して「適合に関わる違反」を 0 にし、best-practice は別枠で扱うと、2.5.8 を見落としません。
自動で検出できる範囲の目安と、手動確認の手順
axe-core の開発元 Deque は、2021年3月10日公開の調査で「自動テストはアクセシビリティ課題の 57% を検出できる」と報告しています。 2,000 件超の監査、13,000 ページ超、約 30 万件の課題を対象に、達成基準の数ではなく課題の件数で測った結果です。
"when we shifted the definition of 'accessibility coverage' beyond the number of WCAG Success Criteria to total volume of issues, the true impact of automated testing became clear"(カバー率の定義を達成基準の数から課題の総量に変えたとき、自動テストの本当の効果が明らかになった)
つまり 件数では半分以上を自動で拾えますが、項目では 55 のうち 35 が手動です。次の 5 つを型にすると、1 ページ 15〜30 分で回せます。
| 確認 | 手順 | 合格の目安(主な達成基準) |
|---|---|---|
| キーボードだけで操作 | マウスを使わず Tab / Shift+Tab / Enter / Space / 矢印 / Esc で、メニュー・モーダル・フォーム送信・カルーセルを一通り使う | 全機能に届く、フォーカス位置が常に見える、順序が見た目と一致、閉じ込められない、固定要素に隠れない(2.1.1, 2.1.2, 2.4.3, 2.4.7, 2.4.11) |
| 200% / 400% 拡大 | ブラウザのズームを 200%、次に 400%(幅 1280px なら実質 320px)にする | 文字が切れない・重ならない、横スクロールなしで読める。表やコードなど 2 次元が必要なものは例外(1.4.4, 1.4.10) |
| リフロー | 開発者ツールで幅 320px にする | 横スクロールが出ず、内容と機能が失われない(1.4.10) |
| コントラスト | 開発者ツールのカラーピッカー、または次節の Python で主要な配色を計算 | 通常テキスト 4.5:1、大きなテキスト・アイコン・入力欄の枠・フォーカスリング 3:1(1.4.3, 1.4.11) |
| スクリーンリーダー | Windows は NVDA(無料)、macOS は VoiceOver(Cmd+F5)で先頭から読み上げ、見出しジャンプ(H キー)とフォーム操作 | 画像・ボタン・リンクの名前に意味がある、見出しで構造がわかる、エラーと完了が読み上げられる(1.1.1, 1.3.1, 2.4.6, 3.3.1, 4.1.2, 4.1.3) |
キーボード確認の一部は Playwright で記録できます。 Tab を押すたびに document.activeElement のタグ・type・名前を出力する 15 行ほどのスクリプトで、bad.html は次のようになりました。
$ node taborder.mjs http://localhost:7740/bad.html
1: a "詳細"
2: a ""
3: input[text] ""
4: input[email] ""
5: select[select-one] "会議室A 会議室B"
6: input[checkbox] ""
7: button[button] ""
8: button[button] ""
9: button[button] ""
10: a "こちら"
tabindex="5" の「詳細」が最初に来て(2.4.3)、名前のないリンクとボタンが並び(4.1.2)、<div onclick> の「送信」には一度もフォーカスが当たりません(2.1.1)。修正後の good.html では「送信」ボタンに 7 番目で到達し、順序も見た目どおりになりました。
コントラスト比を計算する(Python、4.5:1 / 3:1 の判定)
コントラスト比は WCAG 2.2 の定義どおり「相対輝度」から計算できます。 sRGB 各成分を 0〜1 にして 0.04045 以下なら 12.92 で割り、それ以外は ((値 + 0.055) / 1.055) の 2.4 乗。相対輝度 L = 0.2126R + 0.7152G + 0.0722B、コントラスト比 = (L1 + 0.05) / (L2 + 0.05)(L1 は明るい方)です。
# contrast.py — WCAG 2.2 のコントラスト比を計算して 4.5:1 / 3:1 / 7:1 を判定(Python 3.11、標準ライブラリのみ)
def linear(c: int) -> float:
s = c / 255
return s / 12.92 if s <= 0.04045 else ((s + 0.055) / 1.055) ** 2.4
def luminance(hex_color: str) -> float:
h = hex_color.lstrip("#")
r, g, b = (int(h[i:i + 2], 16) for i in (0, 2, 4))
return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b)
def contrast(fg: str, bg: str) -> float:
l1, l2 = sorted((luminance(fg), luminance(bg)), reverse=True)
return (l1 + 0.05) / (l2 + 0.05)
def judge(ratio: float) -> str:
return " / ".join(f"{name}:{'OK' if ratio >= th else 'NG'}"
for name, th in [("AA通常(4.5)", 4.5), ("AA大・UI(3)", 3.0), ("AAA(7)", 7.0)])
for fg, bg in [("#999999", "#ffffff"), ("#767676", "#ffffff"), ("#0a5fd0", "#ffffff"), ("#ff9800", "#ffffff")]:
r = contrast(fg, bg)
print(f"{fg} on {bg}: {r:.2f}:1 {judge(r)}")
$ python3 contrast.py
#999999 on #ffffff: 2.85:1 AA通常(4.5):NG / AA大・UI(3):NG / AAA(7):NG
#767676 on #ffffff: 4.54:1 AA通常(4.5):OK / AA大・UI(3):OK / AAA(7):NG
#0a5fd0 on #ffffff: 5.90:1 AA通常(4.5):OK / AA大・UI(3):OK / AAA(7):NG
#ff9800 on #ffffff: 2.16:1 AA通常(4.5):NG / AA大・UI(3):NG / AAA(7):NG
axe が報告した #999999 の 2.84 と、この計算の 2.85 は丸めの違いです。白地のグレーは #767676 が 4.5:1 を満たす最も明るい値(4.54:1)なので、注記の色の下限として覚えておくと便利です。オレンジ(#ff9800)はアイコンや枠線に使っても 3:1 に届かないため、濃くするか輪郭線を足します。
JIS X 8341-3:2016 と 2024年4月の法改正との関係
JIS X 8341-3:2016 の達成基準は WCAG 2.0(ISO/IEC 40500:2012)と同一です。 2.1・2.2 で追加された基準は JIS に含まれないため、WCAG 2.2 AA を満たしていれば JIS の AA 相当も満たし、追加基準の分だけ 2.2 の方が広い関係になります(JIS 向けの試験結果では 4.1.1 を「満たす」として報告します)。
2024年4月1日施行の改正障害者差別解消法では、民間事業者にも「合理的配慮の提供」が義務化されました(それまでは努力義務)。ウェブアクセシビリティの事前対応は同法の「環境の整備」に位置づけられ、こちらは努力義務のままです。つまり WCAG や JIS への準拠そのものが法的義務になったわけではありませんが、個別の求めに応じて代替手段を用意する義務はあり、サイトが使える状態であるほど合理的配慮の負担は小さくなります。
優先順位の付け方(影響が大きい順)とチーム運用(PR でのチェック)
55 項目を一度に満たそうとせず、「使えない」を先に、「使いにくい」を後に直します。 axe の impact(critical / serious / moderate / minor)は目安になり、最終的な順位はページの役割で決めます。
| 優先 | 状態 | 代表的な達成基準 | axe の impact |
|---|---|---|---|
| 1 | キーボードやスクリーンリーダーで 操作・到達できない | 2.1.1 キーボード、2.1.2 トラップ、4.1.2 名前・役割・値(button-name, link-name)、3.3.2 ラベル(label, select-name)、1.1.1 alt(image-alt) | critical |
| 2 | 操作はできるが 内容を読み取れない | 1.4.3 コントラスト、3.1.1 言語、2.4.2 タイトル、1.3.1 見出し・表の構造、3.3.1 エラーの特定 | serious |
| 3 | 使えるが 負担が大きい | 2.4.7 フォーカス表示、2.5.8 ターゲットサイズ、1.4.4 拡大、1.4.10 リフロー、2.4.11 隠されないフォーカス | serious / moderate |
| 4 | 整っていると 快適 | 2.4.1 スキップリンク、2.4.5 複数の経路、3.2.6 一貫したヘルプ、3.3.7 冗長な入力項目 | moderate / best-practice |
ページの順番は「全ページ共通部品(ヘッダー・フッター・ナビ)→ フォームなど目的達成に必須のページ → アクセス上位ページ」の順に直すと、1 回の修正で効く範囲が最大になります。
PR ごとの自動検査(@playwright/test)
自動検査は PR のたびに回し、「WCAG タグの違反 0 件」をマージ条件にします。 @playwright/test 1.56.1 で、baseURL を http://localhost:7740 にした設定で実行しました。
// tests/a11y.spec.mjs — PR ごとに回す WCAG 2.2 AA の自動検査
import { test, expect } from '@playwright/test';
import { AxeBuilder } from '@axe-core/playwright';
const WCAG_AA = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'];
for (const path of ['/good.html', '/bad.html']) {
test(`WCAG 2.2 AA の自動検査: ${path}`, async ({ page }) => {
await page.goto(path);
const results = await new AxeBuilder({ page }).withTags(WCAG_AA).analyze();
// 失敗時に「どのルールが何件」かが一目でわかる形にして比較する
const summary = results.violations.map(v => `${v.id} (${v.impact}) x${v.nodes.length}`);
expect(summary).toEqual([]);
});
}
$ npx playwright test
✓ 1 tests/a11y.spec.mjs:8:3 › WCAG 2.2 AA の自動検査: /good.html (686ms)
✘ 2 tests/a11y.spec.mjs:8:3 › WCAG 2.2 AA の自動検査: /bad.html (657ms)
Error: expect(received).toEqual(expected) // deep equality
- Expected - 1
+ Received + 13
- Array []
+ Array [
+ "autocomplete-valid (serious) x1",
+ "button-name (critical) x3",
+ "color-contrast (serious) x1",
+ "document-title (serious) x1",
+ "html-has-lang (serious) x1",
+ "image-alt (critical) x1",
+ "label (critical) x1",
+ "link-name (serious) x1",
+ "meta-viewport (moderate) x1",
+ "select-name (critical) x1",
+ "target-size (serious) x5",
+ ]
1 failed
1 passed (2.7s)
violations をそのまま toEqual([]) と比べると失敗時の差分が巨大な JSON になります。「ルール id (impact) x件数」の配列に変換してから比べると、PR のログだけで何を直せばよいかがわかります。CI では npm ci → npx playwright install --with-deps chromium → 検査対象をローカル起動 → npx playwright test tests/a11y.spec.mjs の順で動かします(CI 自体はこの記事の検証環境では実行していません)。
| 運用ルール | 内容 |
|---|---|
| 新規ページ | WCAG タグの violations 0 件が必須。incomplete は内容を確認し、PR に理由を書く |
| 既存ページ | 件数を記録し、PR ごとに「増やさない・減らす」。disableRules で一時除外するときは期限を書く |
| 手動チェック | PR テンプレートに 5 項目(キーボード・拡大・リフロー・コントラスト・読み上げ)を置き、UI を変えた PR だけ記入 |
| 定期巡回 | 月 1 回、アクセス上位ページを 5 つの手動確認で巡回し、結果を課題に起こす |
よくある質問
Q. 目標は AA と AAA のどちらにすべきですか? A. 一般的には AA です。W3C 自身が、AAA はすべてのコンテンツで満たせるとは限らないため、方針として全体に AAA を求めることを推奨していません。まず A・AA の 55 項目を満たし、AAA は 1.4.6 コントラスト(高度)など効果の大きいものを選んで加えます。
Q. axe で違反 0 件なら WCAG 2.2 に準拠したと言えますか? A. 言えません。この記事の区分では 55 項目中 35 項目は手動確認が必要で、Deque も課題の件数ベースで約 57% が自動検出の範囲としています。0 件は「手動確認に進める状態」と考えます。
Q. WCAG 2.1 で AA 対応済みのサイトは、2.2 で何を追加すればよいですか? A. AA までなら 2.4.11、2.5.7、2.5.8、3.2.6、3.3.7、3.3.8 の 6 項目です。4.1.1 は廃止されたので確認不要です。固定ヘッダー、ドラッグ操作、小さいアイコンボタン、ログイン画面、複数ステップのフォームがあるページから見ると効率的です。
Q. placeholder だけのフォームは axe を通りましたが、問題ありませんか?
A. axe の label ルールは placeholder をアクセシブルな名前として受け入れますが、入力を始めると消え、拡大時に読みにくくなるため、見える <label> を付けることを推奨します。自動検査を通ることと、3.3.2 を十分に満たすことは別物です。
まとめ
- WCAG 2.2 は全 86 項目、目標にする A・AA は 55 項目(廃止の 4.1.1 を含めると 56 行)。2.2 で 9 項目が追加され、AA までの追加は 6 項目
- 自動検査で手がかりが得られるのは 55 項目中 20 項目。Deque の調査でも件数ベースで約 57% なので、手動確認は必須
- axe-core はタグを明示して実行する。既定では best-practice が混ざり、2.5.8 の
target-sizeは走らない - @axe-core/playwright は
browser.newContext()経由のページが必須。newPage()直接では "Please use browser.newContext()" で止まる - 手動確認は 5 つの型(キーボード・200%/400% 拡大・320px リフロー・コントラスト・スクリーンリーダー)。Tab 順の記録は Playwright で補助できる
- コントラストは相対輝度から計算できる。白地のグレーは
#767676が 4.5:1 の下限、UI 部品は 3:1 - JIS X 8341-3:2016 は WCAG 2.0 相当。2024年4月の法改正は合理的配慮の義務化であり、WCAG 準拠の義務化ではない
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「WCAG 2.2チェックリスト|A・AA全55項目と確認方法・axe検査手順」
株式会社RJCでは、アクセシビリティの自動検査と手動確認をプロジェクトの中で仲間と学び合い、誰もが使える画面づくりを実践しています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。