コントラスト比「4.50:1」なのにWCAG AAで不合格になる理由

文字色と背景色のコントラスト比が「4.50 : 1」と表示されているのに、WCAG の AA(通常テキストは 4.5:1 以上)では不合格になる。表示の誤りではなく、WCAG の規則どおりの結果である。

ハシトシステムの コントラスト比チェッカー は、比を小数2桁で表示し、合否は丸める前の値で判定している。この記事では、公開中のツールの関数を Node.js で取り出して、白背景の 16,777,216 色すべてについて「表示は 4.50 なのに不合格」になる色を数える。あわせて、相対輝度の式にある閾値 0.03928 と 0.04045 の違いが結果を変えるかも確かめる。

WCAG は「比を丸めずに比べる」と書いている

W3C の Understanding WCAG 2.2(達成基準 1.4.3 コントラスト(最低限))は、3:1 と 4.5:1 を閾値として扱い、計算した比は丸めずに比べるとしている。例として、4.499:1 は 4.5:1 を満たさないと明記している。

つまり、表示用に小数2桁へ丸めた「4.50」で判定してはいけない。ツールのコードも、表示と判定を分けている。

1
2
3
// 2026-11-05 時点の contrast-checker のコード(抜粋)
$('ratio').textContent = r.toFixed(2) + ' : 1'; // 表示は小数2桁
mark($('aaN'), r >= 4.5, 'AA 通常テキスト (4.5:1)'); // 判定は丸める前の r

toFixed(2) は 4.495 以上 4.5 未満を「4.50」と表示する。この範囲の色は、表示が 4.50 で判定が FAIL になる。

白背景で「4.50 なのに不合格」は 9,074 色

Node.js v24.11.1 で、ツールの ratio 関数に白(#ffffff)と全 1,677 万色を渡した結果が次のとおり(コードは後ろの「実際に試す」にある)。

文字色(白背景) 比(丸める前) 表示 AA 通常テキスト(4.5) AA 大きい文字(3)
#777777 4.478089 4.48 : 1 FAIL PASS
#767676 4.542225 4.54 : 1 PASS PASS
#006ffb 4.499888 4.50 : 1 FAIL PASS
#0099ff 2.999789 3.00 : 1 FAIL FAIL
条件(白背景・全 16,777,216 色) 色の数
表示が「4.50」で AA 通常テキスト不合格(4.495 以上 4.5 未満) 9,074
表示が「3.00」で AA 大きい文字不合格(2.995 以上 3 未満) 13,242

白背景のグレーで AA を満たす最も明るい色は #767676 で、#777777 は届かない。青系の #006ffb は比が 4.499888 で、小数2桁では 4.50 と表示されるが不合格になる。デザインツールや別のチェッカーが比を丸めて表示していて、「4.50 だから合格」と読むと、この範囲の色を通してしまう。

数字の見た目に頼らない対策は2つある。判定する側は、丸めた文字列ではなく丸める前の数値で >= 4.5 と比べる。色を決める側は、4.5 ちょうどを狙わず、少し余裕を持った色を選ぶ。

0.03928 と 0.04045 は 8bit の色では結果を変えない

相対輝度は、sRGB の各チャンネル(0〜255 を 255 で割った値)を線形の値に戻してから重み付きで足す。戻す式は、値が閾値以下なら c / 12.92、それより大きければ ((c + 0.055) / 1.055) ^ 2.4 である。

この閾値は資料によって 0.03928 と 0.04045 の2通りが出てくる。WCAG 2.2 の用語集の相対輝度の定義は現在 0.04045 で、注記に、2021年5月より前は 0.03928 だったこと、この変更はガイドラインの文脈では計算に実質的な影響がないことが書かれている。CSS Color Module Level 4 の sRGB の定義も 0.04045 を使っている。ツールのコードは古い値の 0.03928 のままである。

影響がない理由は、8bit の色で閾値の間に入る値が無いからである。

8bit の値 255 で割った値 0.03928 以下か 0.04045 以下か
10 0.03922 はい はい
11 0.04314 いいえ いいえ

10 と 11 の間に 0.03928 と 0.04045 の両方が入るので、0〜255 のどの値でもどちらの閾値でも同じ式が選ばれる。実際に 256 値すべてで線形化した結果を比べると、違いは 0 件だった。16bit の色や、0〜1 の小数で色を扱う場合は、0.03928 と 0.04045 の間に入る値があり得るので、現行の 0.04045 を使っておくのが無難である。

大きい文字の 18pt と 14pt は px でいくつか

AA の閾値は、大きい文字なら 3:1 に下がる。WCAG の大きい文字は、18pt 以上、または太字の 14pt 以上である。CSS では 1pt = 96/72 px なので、18pt は 24px、14pt は 18.667px になる。Understanding WCAG 2.2 は 14pt を「およそ 18.5px」と書いており、ツールの FAQ は 18.66px と書いている。境目の太字 18px や 18.5px の文字は 14pt に届かないので、大きい文字としては扱えない。

実際に試す

Node.js 18 以降(グローバルの fetch を使うため。記事では v24.11.1)で試せる。外部パッケージは使わない。全 1,677 万色のループがあるので、終わるまで少し待つ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// try.mjs: 公開中のコントラスト比チェッカーの関数を取り出して、丸めと閾値の境目を調べる
const html = await (await fetch('https://hashitosystem.com/tools/contrast-checker/')).text();
const s = html.indexOf('function toRGB(');
const e = html.indexOf('function mark(', s);
const { toRGB, lin, ratio } = new Function(html.slice(s, e) + '; return { toRGB, lin, ratio };')();

// 1. 閾値 0.03928(旧)と 0.04045(現行)で、8bit の 256 値の線形化結果が変わるか
const linNew = (c) => { c = c / 255; return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); };
let diff = 0;
for (let v = 0; v < 256; v++) if (lin(v) !== linNew(v)) diff++;
console.log('閾値で結果が変わる 8bit 値の数:', diff);
console.log('10/255 =', (10 / 255).toFixed(5), ' 11/255 =', (11 / 255).toFixed(5));

// 2. 白背景のグレー文字: 表示(toFixed(2))と AA 判定(r >= 4.5)
const show = (fg, bg) => {
const r = ratio(toRGB(fg), toRGB(bg));
console.log(`${fg} on ${bg}`.padEnd(20), r.toFixed(6).padEnd(10), '表示', (r.toFixed(2) + ' : 1').padEnd(10), 'AA通常', r >= 4.5 ? 'PASS' : 'FAIL', ' AA大', r >= 3 ? 'PASS' : 'FAIL');
};
for (const g of ['#777777', '#767676', '#757575']) show(g, '#ffffff');

// 3. 表示は 4.50 / 3.00 なのに不合格になる色を、白背景の全 1677 万色から数える
let n45 = 0, n30 = 0; const ex45 = [], ex30 = [];
const W = [255, 255, 255];
for (let i = 0; i < 1 << 24; i++) {
const rgb = [i >> 16, (i >> 8) & 255, i & 255];
const r = ratio(rgb, W);
if (r < 4.5 && r.toFixed(2) === '4.50') { n45++; if (ex45.length < 3) ex45.push('#' + i.toString(16).padStart(6, '0')); }
if (r < 3 && r.toFixed(2) === '3.00') { n30++; if (ex30.length < 3) ex30.push('#' + i.toString(16).padStart(6, '0')); }
}
console.log('白背景で「4.50」表示なのに AA 不合格:', n45, '色 例', ex45.join(' '));
console.log('白背景で「3.00」表示なのに 大AA 不合格:', n30, '色 例', ex30.join(' '));
for (const c of [...ex45.slice(0, 1), ...ex30.slice(0, 1)]) show(c, '#ffffff');

// 4. 大きい文字の境目: pt を CSS px に(1pt = 96/72 px)
for (const pt of [14, 18]) console.log(`${pt}pt =`, (pt * 96 / 72).toFixed(3), 'px');
1
2
3
4
5
6
7
8
9
10
11
12
$ node try.mjs
閾値で結果が変わる 8bit 値の数: 0
10/255 = 0.03922 11/255 = 0.04314
#777777 on #ffffff 4.478089 表示 4.48 : 1 AA通常 FAIL AA大 PASS
#767676 on #ffffff 4.542225 表示 4.54 : 1 AA通常 PASS AA大 PASS
#757575 on #ffffff 4.607518 表示 4.61 : 1 AA通常 PASS AA大 PASS
白背景で「4.50」表示なのに AA 不合格: 9074 色 例 #006ffb #0071f4 #0076e0
白背景で「3.00」表示なのに 大AA 不合格: 13242 色 例 #0099ff #009afa #009bf5
#006ffb on #ffffff 4.499888 表示 4.50 : 1 AA通常 FAIL AA大 PASS
#0099ff on #ffffff 2.999789 表示 3.00 : 1 AA通常 FAIL AA大 FAIL
14pt = 18.667 px
18pt = 24.000 px

ページのコードが変われば結果も変わるので、2026-11-05 時点の結果として読んでほしい。背景を白以外にしたいときは、W の値を変える。

ハシトシステムのコントラスト比チェッカーでは

コントラスト比チェッカー は、文字色と背景色を HEX で入れると、比と4つの合否(AA / AAA × 通常テキスト / 大きい文字)をブラウザの中だけで出す。2026-11-05 時点のコードでは次のように動く。

  • 比は小数2桁で表示し、合否は丸める前の値で判定する。#006ffb を白背景で入れると「4.50 : 1」と表示され、AA 通常テキストは FAIL になる。表示が 4.50 や 3.00 ちょうどのときは、合否の表示のほうを見る。
  • 相対輝度の閾値は旧来の 0.03928 だが、HEX(8bit)の入力では現行の 0.04045 と結果が変わらない。
  • #RGB の3桁の HEX も受け付け、#777 は #777777 として計算する。

QR コードの色で読めるかどうかを相対輝度から考える話は 赤と黒の QR コードが読めない理由の記事 に書いた。QR コードの配色は QRコード 配色チェッカー で、色の HEX と RGB の変換は カラーコード変換 で確かめられる。

まとめ

  • WCAG のコントラスト比は丸めずに閾値と比べる。4.499:1 は 4.5:1 を満たさない。
  • 小数2桁の表示では 4.495 以上 4.5 未満が「4.50」になる。白背景だけで 9,074 色が「4.50 なのに AA 不合格」、13,242 色が「3.00 なのに大きい文字の AA 不合格」だった。
  • 白背景のグレーで AA を満たす最も明るい色は #767676。#777777 は 4.48 で届かない。
  • 相対輝度の閾値は現行の WCAG 2.2 では 0.04045(2021年5月より前は 0.03928)。8bit の色では 10/255 と 11/255 の間に両方が入るので、どちらでも結果は同じ。
  • 大きい文字は 18pt(24px)以上か、太字 14pt(18.667px)以上。

参考

  • W3C Web Content Accessibility Guidelines (WCAG) 2.2 — Glossary: relative luminance、large scale (text)
  • W3C Understanding WCAG 2.2 — Understanding Success Criterion 1.4.3: Contrast (Minimum)
  • W3C CSS Color Module Level 4 — 10.2 The Predefined sRGB Color Space