本文へスキップ
Articles

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: 1flex-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つずつ確認しました。近くの文字がそのまま使えたものが大半でしたが、選択肢の一覧が拾われてしまうものもあり、そこは自分で決めています。抽出は機械、判断は手作業という分け方が現実的でした。

持ち帰れること

結果として、違反は818件から2件になりました。ただ、この記事で価値があるのは件数ではありません。「完成していると思っていたものを測ったら、4種類の見えない欠陥が出た」という事実のほうです。測っていないサイトには、たぶん同じものがあります。