はじめに:なぜ「感覚の配色」は炎上するのか
Webアクセシビリティの現場において、最も頻繁に遭遇し、かつ修正の手戻りが大きいのが**「テキストと背景色のコントラスト比不足」**です。
2024年4月の障害者差別解消法の改正法施行に伴い、民間事業者においてもWebアクセシビリティへの合理的配慮が求められる中、「デザインカンプ通りに実装したらAA基準(4.5:1)を満たしていなかった」「ブランドカラーを保ったままどう調整すればいいか分からない」という課題を抱えるエンジニア・デザイナーは少なくありません。
本記事では、W3C WCAG 2.2 勧告に完全準拠したコントラスト比・相対輝度の計算アルゴリズムと、基準未達時に色相(Hue)や彩度(Saturation)を極力崩さずに基準値をクリアする**「二分探索(Binary Search)による補正候補色探索アルゴリズム」**の実装について、一次資料の定義に沿って解説します。
1. W3C WCAG 2.2における「相対輝度」と「コントラスト比」の厳密な定義
コントラスト比は、単なるRGB値の差や明度(HSV/HSLのValueやLightness)の単純な比較ではありません。人間の目の比視感度特性と、ディスプレイのsRGB色空間におけるガンマ特性(約2.2乗)を考慮した**「相対輝度(Relative Luminance: $L$)」**に基づいて定義されています。
相対輝度の計算ステップ(W3C勧告準拠)
-
8ビットRGB(0〜255)を 0.0〜1.0 に正規化する
$$R_s = R / 255,\quad G_s = G / 255,\quad B_s = B / 255$$ -
sRGB ガンマ補正を解除(リニアRGBへの逆変換)
W3Cの定義式に基づき、各色成分 $C_s \in {R_s, G_s, B_s}$ に対して以下の区分線形変換を適用します:$$C = \begin{cases} \dfrac{C_s}{12.92} & (C_s \le 0.04045) \[8pt] \left(\dfrac{C_s + 0.055}{1.055}\right)^{2.4} & (C_s > 0.04045) \end{cases}$$
-
ITU-R BT.709 係数による加重平均で相対輝度 $L$ を算出
人間の目が緑に最も敏感で青に鈍感であることを反映した重み付けを行います:
$$L = 0.2126 \times R + 0.7152 \times G + 0.0722 \times B$$
($L$ の範囲は最も暗い黒の $0.0$ から、最も明るい白の $1.0$ までとなります)
コントラスト比の計算式
2つの色の相対輝度をそれぞれ $L_1$(明るい方)および $L_2$(暗い方)とするとき、コントラスト比 $CR$ は以下で定義されます:
$$CR = \frac{L_1 + 0.05}{L_2 + 0.05}$$
- 分母と分子に足されている $+0.05$ は、完全な黒($L=0$)でのゼロ除算を防ぐとともに、環境光(アンビエント光)によるフレアを考慮した定数です。
- コントラスト比は常に $1:1$(同じ色)から $21:1$(純黒と純白)の範囲に収まります。
JavaScriptによる純粋関数実装
/**
* 16進数カラーコードをRGBオブジェクトに変換
*/
function hexToRgb(hex) {
const cleanHex = hex.replace(/^#/, '');
const num = parseInt(cleanHex.length === 3
? cleanHex.split('').map(c => c + c).join('')
: cleanHex, 16);
return {
r: (num >> 16) & 255,
g: (num >> 8) & 255,
b: num & 255
};
}
/**
* W3C WCAG 2.2 相対輝度(Relative Luminance)の計算
*/
function getRelativeLuminance(r, g, b) {
const [rs, gs, bs] = [r, g, b].map(c => {
const s = c / 255;
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
/**
* 2色間のコントラスト比を計算
*/
function getContrastRatio(hex1, hex2) {
const rgb1 = hexToRgb(hex1);
const rgb2 = hexToRgb(hex2);
const l1 = getRelativeLuminance(rgb1.r, rgb1.g, rgb1.b);
const l2 = getRelativeLuminance(rgb2.r, rgb2.g, rgb2.b);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
2. 現場での盲点:#64748B(スレートグレー)はAA合格か?
Tailwind CSSの text-slate-500(#64748B)は、モダンなWeb UIで非常によく使われるニュートラルグレーです。
これを白背景(#FFFFFF)の上に配置した場合:
-
#64748BのRGB:(100, 116, 139) - 相対輝度 $L$: 約 $0.1706$
- 白背景
#FFFFFFの相対輝度 $L$: $1.0000$ - コントラスト比: $(1.0 + 0.05) / (0.1706 + 0.05) \approx 1.05 / 0.2206 \approx \mathbf{4.76 : 1}$
WCAG 2.2 における通常の本文テキスト(18pt未満、または14pt未満の太字)の**レベルAA適合基準は「4.5:1以上」**です。
したがって、#64748B × #FFFFFF は 4.76:1 で通常文字のAA基準を満たしており、補正の必要はありません。
目視の感覚だけで「少し薄いから未達だろう」「補正しなければならない」と決めつけたり、誤った計算式で不合格判定を出してしまうと、ブランドガイドラインや統一感を無駄に崩す原因になります。正確な数学モデルで判定することが重要です。
3. 二分探索(Binary Search)による「補正候補色」の探索アルゴリズム
コントラスト比が基準値(例: 4.5:1)を下回っていた場合、どうやって補正色を求めるべきでしょうか?
単に「黒っぽくする」「白っぽくする」だけでは、デザイナーが設定した**色相(Hue: 赤み・青み)や彩度(Saturation)**が失われてしまいます。
そこで、HSL色空間において色相 $H$ と彩度 $S$ を固定し、明度 $L_{hsl}$ だけを二分探索(Binary Search)で微調整するアプローチを採用します。
アルゴリズムの流れ
-
補正方向の決定:
- 背景色が明るい場合(白系)$\rightarrow$ 文字色を「暗くする方向」($L_{hsl} \to 0$)へ探索。
- 背景色が暗い場合(黒系)$\rightarrow$ 文字色を「明るくする方向」($L_{hsl} \to 1$)へ探索。
-
二分探索の実行:
- 探索範囲 $[low, high]$ を設定。
- 反復(Iteration: 10〜15回程度)で中間明度 $mid$ を求め、RGBに変換してコントラスト比を評価。
- 目的の基準比率(4.5:1)を僅かに上回る最も近い明度を特定。
実装コード
/**
* HSLからRGBへの変換
*/
function hslToRgb(h, s, l) {
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
const m = l - c / 2;
let r = 0, g = 0, b = 0;
if (0 <= h && h < 60) { r = c; g = x; b = 0; }
else if (60 <= h && h < 120) { r = x; g = c; b = 0; }
else if (120 <= h && h < 180) { r = 0; g = c; b = x; }
else if (180 <= h && h < 240) { r = 0; g = x; b = c; }
else if (240 <= h && h < 300) { r = x; g = 0; b = c; }
else if (300 <= h && h < 360) { r = c; g = 0; b = x; }
return {
r: Math.round((r + m) * 255),
g: Math.round((g + m) * 255),
b: Math.round((b + m) * 255)
};
}
/**
* 二分探索でターゲット比率を満たす補正候補色を探索
*/
function findAdjustedColor(textHex, bgHex, targetRatio = 4.5) {
const currentRatio = getContrastRatio(textHex, bgHex);
if (currentRatio >= targetRatio) {
return { adjustedHex: textHex, ratio: currentRatio, needed: false };
}
const bgRgb = hexToRgb(bgHex);
const bgL = getRelativeLuminance(bgRgb.r, bgRgb.g, bgRgb.b);
const textHsl = rgbToHsl(hexToRgb(textHex));
// 背景が明るければ暗い方向(0へ)、暗ければ明るい方向(1へ)
const shouldDarken = bgL > 0.5;
let low = shouldDarken ? 0.0 : textHsl.l;
let high = shouldDarken ? textHsl.l : 1.0;
let bestHex = textHex;
let bestRatio = currentRatio;
for (let i = 0; i < 15; i++) {
const mid = (low + high) / 2;
const testRgb = hslToRgb(textHsl.h, textHsl.s, mid);
const testHex = rgbToHex(testRgb.r, testRgb.g, testRgb.b);
const ratio = getContrastRatio(testHex, bgHex);
if (ratio >= targetRatio) {
bestHex = testHex;
bestRatio = ratio;
// より元の色に近い方へ探索範囲を狭める
if (shouldDarken) low = mid;
else high = mid;
} else {
if (shouldDarken) high = mid;
else low = mid;
}
}
return { adjustedHex: bestHex, ratio: bestRatio, needed: true };
}
この二分探索により、15ステップ以内で元の色合いを崩さない「最小限の補正候補色」をミリ秒単位で瞬時に割り出すことができます。
4. ブラウザで即座に試せる検証ツールとして公開しました
この計算式・補正候補色探索アルゴリズムを実装し、実務でワンクリック検証できるツールとして公開しています。
- ツール名: WCAG 2.2 コントラスト比チェッカー&補正候補色探索
-
特徴:
- W3C勧告準拠の相対輝度計算(通常文字AA 4.5:1 / 大文字AA 3.0:1 / AAA 7.0:1)
- 二分探索による補正候補色の自動算出&ワンクリックCSSコピー
- 監査記録用・チーム共有用のA4印刷/PDFプレビュー出力対応
- 登録不要・完全無料・データ外部送信なし(ブラウザ完結)
さらに、タップターゲット領域(24px / 44px)や行送り検証、YouTubeショート/TikTok/リールのUI被り防止シミュレータなど、Web・フロントエンド制作実務に必要な検証ツールをまとめたハブも開設しています:
日々のUIデザインレビューやフロントエンド実装のアクセシビリティ担保にお役立ていただければ幸いです!