2026-09-02 公開 / やらかした話
見た目は完成していたサイトを機械で測ったら、アクセシビリティ違反が818件出た
自分のサイト103ページを、ヘッドレスブラウザと検査ライブラリで一括検査しました。目視では一度も気づかなかった不具合が4種類、まとめて出てきました。どれも「デザインが変わったのに、その前提で書かれたCSSが取り残された」形をしています。人間の目には見えず、ブラウザもエラーを出しません。再現方法と、直し方をどう決めたかを書きます。
何をしたか: 全ページを機械にかける
やったことは単純です。ヘッドレスブラウザで全ページを開き、検査ライブラリを注入して、WCAG 2.1 の A / AA に相当するルールで走らせました。加えて、横スクロールの発生とコンソールエラーも同時に拾います。
const { chromium } = require('playwright');
const AXE = require('fs').readFileSync(require.resolve('axe-core/axe.min.js'), 'utf8');
const browser = await chromium.launch();
const page = await (await browser.newContext({ viewport: { width: 1440, height: 900 } })).newPage();
await page.goto(url, { waitUntil: 'networkidle' });
await page.addScriptTag({ content: AXE });
const result = await page.evaluate(async () => await window.axe.run(document, {
runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] },
resultTypes: ['violations'],
}));
これだけで、1ページあたり数秒です。103ページで数分。最初の結果は違反ノード818件、違反ゼロのページは20ページだけでした。自分では「もう整っている」と思っていたサイトです。
ひとつ注意があります。ページに「スクロールに合わせて要素が現れる」演出があると、検査時に要素が透明のままになり、判定がずれます。ブラウザ側で「動きを減らす」設定を有効にしてから測ります。スクリーンショットを撮る場合も同じで、これをしないと本文が真っ白の画像が量産されます。
await browser.newContext({ reducedMotion: 'reduce', colorScheme: 'dark' });
出てきた不具合1: 白い文字が、明るい地に取り残されていた
あるツールのページで、本文の説明文とページ唯一の見出しがコントラスト比 1.02:1 と報告されました。ほぼ同じ色ということです。実際に見に行くと、明るいクリーム色の余白に見えていた場所に、白い文字が置かれていました。
原因は履歴でした。そのブロックは以前暗い背景の上にあり、そのときは白文字で正しかったのです。後からページ全体の地の色を明るくしたときに、このブロックの色指定だけが取り残されました。
厄介なのは、そこに h1 が入っていたことです。ページで唯一の見出しが、人間には見えず検索エンジンには読める状態で置かれていました。意図していなくても、外から見れば隠しテキストです。見出しはページ上部の実際のタイトルへ移し、下のブロックは h2 と補足色に直しました。
出てきた不具合2: @import が、仕様どおり無視されていた
あるディレクトリだけ、共通の配色ファイルが効いていませんでした。CSSはこう書かれていました。
*,*::before,*::after{box-sizing:border-box}
@import url("/assets/tokens.css"); /* ← 読み込まれない */
@import は、他のルールより前に書かないと無視されます。仕様どおりの挙動なので、コンソールにも何も出ません。ファイルは取得すらされません。
そして、このページ群は見た目上は正常でした。各変数にフォールバック値が書いてあり、それが共通ファイルの値と同じだったからです。色は合っている。合っていないのは、共通ファイルにしかないもの――キーボード操作時の枠線、本文へスキップするリンクの装飾、長いファイル名の折り返し――でした。目に見える部分が正しいので、誰も疑いません。
確認は、色を見るのではなく、読み込まれたかを直接聞くのが確実です。
await page.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue('--ks-bg').trim()
);
// "" が返ってきたら、共通ファイルは読まれていない
出てきた不具合3: 子だけに flex の指定があり、親のルールが無かった
入力欄の並びが縦に積まれて崩れていました。CSSを見ると、子要素には flex: 1 や flex-shrink: 0 が書いてあるのに、親を display: flex にするルールがどこにも無いのです。書き忘れたまま、そのまま公開されていました。
同じページで、削除ボタンの見た目の指定がボタンではなく、それを囲む div に当たっていました。中のボタンは素のままなので、ブラウザ既定の枠付きボタンとして描画されます。私はそれを長いあいだ「画像が読めていない印」だと思って見過ごしていました。
どちらも、flex の子プロパティだけが残っている/クラス名が意図と1階層ずれているという形です。CSSはエラーを出しません。効かないだけです。
出てきた不具合4: opacity で無効を表すと、キーボードでは押せる
店舗検索のツールで、条件が揃うまで押せないボタンがありました。実装はこうです。
.dist-seg { opacity:.4; pointer-events:none }
.dist-seg.ready{ opacity:1; pointer-events:auto }
マウスでは押せません。ですがキーボードでは Tab で入り、Enter で押せます。pointer-events が止めるのはポインタだけだからです。支援技術にも「無効」とは伝わりません。
直し方は色ではなく状態の持ち方でした。disabled を正とし、条件が揃った時点で見た目と disabled をまとめて解除します。ついでに、無効な部品はコントラスト比の対象外になります(WCAG に明示の除外規定があります)。見た目を濃くして誤魔化す必要はありません。
function distReady(){
const box = document.getElementById('dist');
box.classList.add('ready');
box.querySelectorAll('button').forEach(b => { b.disabled = false; });
}
直し方をどう決めたか
採用: 共通の値を1つ動かす
違反748件のうち大半は、同じ組み合わせでした。アクセント色を淡い同系色の地に載せた小さなバッジで、4.29:1。基準の 4.5:1 をわずかに割っています。83ページに散っていましたが、共通ファイルの色を1つ、少し暗くするだけで全部が基準を満たしました。
/* 淡色地の上 4.29:1 → 4.78:1、地の上 4.53 → 5.04、白文字を載せて 4.99 → 5.55 */
--ks-accent: #237375; /* 変更前は #267b7d */
色相はそのままで、明度だけ3%ほど落としています。並べて比べなければ違いは分かりません。1ファイルを1行直して83ページが直るのは、共通化しておいた分の利息です。
却下: 「基準を満たすまで暗くする」を機械的にやること
比率を計算して自動で置換すれば早いのですが、そのままやると内容まで書き換わります。実際、一括置換の除外指定を1つ間違えて、過去記事の本文に書いてあった色の値まで置換してしまいました。「この色は 4.99:1 だった」という実測値の記録が、別の色の値に書き換わっていたわけです。差分を見て気づき、その2本は元に戻しました。
色見本のページも同じ理由で対象外にしました。あそこに並んでいる色は、装飾ではなく内容です。基準を満たすように変えたら、見本として嘘になります。検査結果は2件残っていますが、これは残すと決めた2件です。
却下: 全ページを一度にダークモード対応すること
サイトの大半はダークモードに対応していますが、ツール本体のページ群は明るいままです。今回まとめて対応することも考えて、やめました。ツール1本あたり50〜80個の色が直接書かれており、機械的に暗くすると明暗が混ざった読めない画面になります。動いているツールを壊す危険のほうが大きい。
代わりに、境目を1段だけ動かしました。紹介ページはダーク、ツール一覧は白、という段差があったので、一覧のほうをダークに対応させ、切り替わる場所を「ツールを開いた瞬間」に揃えました。全部やるか何もしないかの二択にしない、というだけの話です。
フォームの部品にラベルが無い、は静かに溜まる
もう1種類、まとめて出たものがあります。入力欄や選択欄103個に、プログラムから読めるラベルが付いていませんでした。画面上には「QRサイズ」「誤り訂正」のような見出しがちゃんと置いてあります。人間には分かります。ただ、その文字と入力欄が結び付けられていないので、読み上げでは「編集テキスト」としか伝わりません。
数が多いので、まず「ラベルが無い部品」と「その近くにある文字」を機械に列挙させ、そのうえで1つずつ確認しました。近くの文字がそのまま使えたものが大半でしたが、選択肢の一覧が拾われてしまうものもあり、そこは自分で決めています。抽出は機械、判断は手作業という分け方が現実的でした。
持ち帰れること
- 目視レビューは、この4種類を1つも見つけられません。白文字は余白に見え、読み込まれていないCSSは正常に見え、効いていない指定は最初からそう見えます。人間は「そこに何も無い」ことを見つけるのが苦手です
- ブラウザはCSSのエラーを出しません。
@importの位置も、親の指定漏れも、クラス名のずれも、すべて「静かに効かない」だけです。だから機械に測らせる必要があります - 不具合の多くは「取り残し」です。デザインを変えたとき、変えた側は確認します。変えていない側に、前の前提のまま残ったものが問題になります
- 共通化してあると、1行で広く直せます。逆に言えば、共通化していないものは、同じ数だけ手作業になります
- 一括置換は、内容まで書き換えます。過去の記録や見本のように「その値であること自体が意味を持つ」場所は、置換対象から外します。外し忘れは差分で見つかるので、必ず差分を読みます
- 「無効」は見た目ではなく状態で表します。薄くするだけでは、キーボードからは押せてしまいます
- 直せない指摘を残す判断も要ります。ゼロにすること自体を目的にすると、内容のほうを歪めます
結果として、違反は818件から2件になりました。ただ、この記事で価値があるのは件数ではありません。「完成していると思っていたものを測ったら、4種類の見えない欠陥が出た」という事実のほうです。測っていないサイトには、たぶん同じものがあります。