ウェブの文字色を確認するとき、ツールがコントラスト比を 4.50 と表示すれば、「4.5:1 に達した」と考えがちです。しかし、表示値には通常、整形が入っています。元の計算結果は 4.499795 かもしれません。文字のコントラスト比はどう計算し、なぜ丸めてから判定してはいけないのでしょう。また、合格した配色表によって実際のページの問題が隠れないようにするには、何を確認すればよいのでしょうか。

再現可能な計算と、実際のページの確認範囲を分けて説明します。規則の出典は W3C の WCAG 2.2 と補足文書、数値はこのコンピューターで実行したスクリプト、修正の進め方は結果に基づく提案です。人の読みやすさを測定したり、谷米や他のサイトの包括的なアクセシビリティ監査を行ったりはしていません。

1. どの種類の文字を判定するか決める

WCAG 2.2 の達成基準 1.4.3 はレベル AA に属します。一般の文字と背景のコントラスト比は 4.5:1 以上、大きな文字では 3:1 以上です。ここで扱うのはこの基準の色に関する要件であり、ロゴや純粋な装飾などには別の例外があります。すべての視覚要素に一律に適用する数値ではありません。WCAG 2.2:Contrast (Minimum)

「大きな文字」も、コンポーネントを見出しと呼ぶだけでは判断できません。定義には 18pt 以上の文字、14pt 以上の太字、または中日韓のフォントで同等の大きさになる文字サイズが含まれます。実際のフォントとサイズを確認し、h1 というだけで低い閾値を選んだり、利用者が拡大した状態を配信時の大きな文字と扱ったりしないことです。WCAG 2.2:large-scale の定義

計算機は 4.5 と 3 の両方について判定できますが、二つの色だけではページにどちらを適用すべきか決まりません。以下の「合格」は、数値が指定した閾値に達したという意味です。サイトの AA 適合にはページ全体と他の達成基準も関係します。一組の色の判定には、その範囲はありません。W3C:適合要件の解説

2. 比較するのは相対輝度

入力を #767676 のような、不透明な六桁の十六進数 sRGB 色に限定します。八ビットの各チャンネルを 255 で割り、区分された関数で線形の値へ変換します。その後、赤、緑、青の重みで相対輝度を求めます。最後に、明るい方の輝度へ 0.05 を足し、暗い方の輝度へ 0.05 を足した値で割ります。W3C の技術文書 G18 にこの手順があります。W3C:G18 の計算手順

線形へ変換する理由は数値からも分かります。sRGB のチャンネルは符号化された値で、線形の輝度として直接使えません。スクリプトで #808080 を計算すると、正規化されたチャンネルは約 0.501961、相対輝度は約 0.215861 でした。前者を後者として使うと、計算したい量そのものが変わります。最後の割り算が正しくても修復できません。

次は完全なスクリプトにある計算関数です。透明色、省略表記、他の色空間を明示的に拒否し、非対応の入力から精密に見える数値を黙って作ることを防ぎます。

import re

def parse_hex(color):
    if not isinstance(color, str) or re.fullmatch(r"#[0-9a-fA-F]{6}", color) is None:
        raise ValueError("expected one opaque sRGB color in #RRGGBB format")
    return tuple(int(color[index:index + 2], 16) for index in (1, 3, 5))

def linear_channel(channel):
    value = channel / 255
    return value / 12.92 if value <= 0.04045 else ((value + 0.055) / 1.055) ** 2.4

def luminance(color):
    red, green, blue = (linear_channel(channel) for channel in parse_hex(color))
    return 0.2126 * red + 0.7152 * green + 0.0722 * blue

def contrast_ratio(foreground, background):
    darker, lighter = sorted((luminance(foreground), luminance(background)))
    return (lighter + 0.05) / (darker + 0.05)

def meets_threshold(raw_ratio, threshold=4.5):
    return raw_ratio >= threshold

前景と背景を交換しても式の比は変わりません。同じ色では 1:1、黒と白の両端では 21:1 になります。これらはスクリプトでも確認した性質です。ただし、数値が対称でも、明るい背景に暗い文字と、暗い背景に明るい文字とが、あらゆる読書環境で同じ体験になるという意味ではありません。検証したのは式です。

3. 八つの実際の配色を同じ表で比較する

2026 年 10 月 5 日に、macOS、Python 3.9.6 で完全な実験スクリプトを実行しました。Python の標準ライブラリーだけを使っています。ダウンロード後、同じディレクトリで実行します。

python3 text-contrast-lab.py --json text-contrast-results.json --svg text-contrast-samples.svg

実行結果の JSONには、元の浮動小数点の結果、小数点以下二桁の表示文字列、4.5 と 3 をそれぞれ比較した真偽値を残しています。表の背景はすべて #FFFFFF です。読みやすさのため比を小数点以下六桁で表示し、判定には整形前の計算値を使いました。

前景色コントラスト比の表示値4.5 以上3 以上
#00000021.000000:1はいはい
#7676764.542225:1はいはい
#7777774.478089:1いいえはい
#7079724.499795:1いいえはい
#8888883.544886:1いいえはい
#FF00003.998477:1いいえはい
#00FF001.372190:1いいえいいえ
#0000FF8.592471:1はいはい

白背景の六つの文字色、比率、4.5 の閾値に対する判定。近い灰色でも閾値の両側に分かれる

図の PASS は元の計算値が 4.5 以上、FAIL はそれ未満であることを示します。色のサンプルであり、フォント描画の測定ではありません。完全な数値は表と JSON を参照してください。サンプル SVG を個別にダウンロードすることもできます。

灰色の #767676 と #777777 は、各チャンネルの整数が一つ違うだけで、判定が閾値の両側に分かれます。規則には検証できる境界が必要なので、その近くではこうした状況が生じます。前者が突然すべての人に読みやすくなる証拠でも、後者が誰にも見えなくなる証拠でもありません。最低要件を数値の境界として理解すると、実験で裏付けていない人体への効果を付け加えずに済みます。

4. 表示が 4.50 でも、元の値が 4.5 以上とは限らない

#707972 の実際の比は 4.49979546274635 です。小数点以下二桁に整形すると文字列 "4.50" になります。その文字列を再び数値に変換して比較すると、合格と判定してしまいます。W3C の解説は、計算値を比較するときに丸めないよう明示し、4.499:1 は 4.5:1 に達しないと例示しています。W3C:閾値と丸めの説明

直接的な実装は、計算、判定、表示を分けることです。元の値で合否を決めてから、整形した文字列を画面へ渡します。レポートには色の組、適用した閾値、判定も残します。二桁しか表示しない場合は「4.50、閾値未満」と添えたり、境界付近では桁数を増やしたりできます。短い表示文字列だけから、読者に判定を推測させないことです。

浮動小数点の計算も、閾値を自由に緩める理由にはなりません。比較へ広い許容誤差を加えると、実際に 4.5 未満の色も合格にしてしまう可能性があります。このケースと閾値の差は約 0.000205 で、この大きさでの倍精度数の最後の桁の差よりはるかに大きい値です。スクリプトでは、ちょうど 4.5、4.499、浮動小数点で 4.5 のすぐ下にある値も確認し、元の値の比較で順に合格、不合格、不合格となりました。

デザインでは、閾値にぎりぎり合わせる代わりに余裕を持たせる選択ができます。たとえば内部の目標を 5:1 に上げるのは、プロジェクト独自の設計上の提案であり、WCAG が強制する要件だと主張するものではありません。背景やスタイルの変更ですぐに閾値を下回る機会を減らせますが、新しい状態の再計算は必要です。

5. RGB の平均値も誤解につながる理由

純粋な赤、緑、青の三チャンネルの算術平均は、どれも 85 です。しかし白背景の比は約 4.00、1.37、8.59 と異なります。平均では、相対輝度を求める際のチャンネルごとの重みの違いが消えます。色相の違い、彩度、三チャンネルの平均だけでは、上述の式と同じ結果は得られません。

重み付けをしても、線形への変換を飛ばすと誤ります。先ほどの #808080 の 0.501961 をそのまま輝度として使えば、白背景の比は約 1.902309 です。正しい手順では約 3.949440 になります。入力する量を誤ったため、大きく違う数値になりました。計算機の実装を調べるときは、最後に 0.05 があるかを検索するだけでなく、変換の手順全体を確認します。

実行では八つの配色に関するアサーションに加え、十五項目の境界確認が通りました。正確な閾値とその直下、同じ色、黒白の両端、前景と背景の交換、変換関数の両方の分岐、256 段階すべての灰色の単調性、小文字の入力、不正な形式、透明色、Display P3 入力の拒否などです。丸め、RGB 平均、符号化された値を直接使う反例もあります。通常の Python 実行モードでアサーションを実行しました。再現時にアサーションを無効にする -O を使わないでください。

6. 計算が正しくても、実際の背景を選ぶ必要がある

単色の関数が知るのは、渡した二つの色だけです。文字が薄い灰色のカード上にあるのに、白いページ背景で計算すれば、確認対象を誤ります。修正を始める際には、具体的な文字、その背後にある背景、フォントの条件、状態を記録し、適用する閾値を選びます。「このブランド色は合格した」と聞いたら、どの背景で、どの文字に使ったのかを確認します。

最も単純な反例は白い文字です。黒背景では比が 21、白いカードに移せば 1 になります。スクリプトでも両端を確認しました。文字色を一切変えなくても、コンテナーの背景が変わるだけで元の合格記録は無効になり得ます。コンポーネントの位置や階層を変えるときも、使う背景との組を再確認します。

透明度、グラデーション、画像があると、入力の問題は広がります。透明な文字の最終的な色は下にある内容で変わり、祖先要素の透明度が全体に影響することもあります。グラデーションに背景色は一つしかないとは言えません。CSS Color 4:要素全体の不透明度 スクリプトは合成や CSS の解析を行いません。その場合は実際のページを正しく扱えるツールで確認するか、文字の背後に制御できる単色の面を置き、そこでの色の組を確認します。八桁の色から透明度のチャンネルを削ってスクリプトへ入れると、別の問題へ置き換えてしまいます。

状態とテーマも確認します。本文色が合格しても、ホバー時のリンク、エラー表示、ダークモードで上書きした文字まで合格する保証はありません。用途に応じた前景と背景の組を定義し、実際に使う組ごとに結果を記録できます。テーマを変える際には組も一緒に更新します。一つの十六進数色を「安全な色」として登録するだけでは、別の背景へ再利用したときに根拠が失われます。

字形も見る必要があります。W3C の解説は、スタイルやユーザーエージェントから得た前景と背景で基準を評価することを勧める一方、アンチエイリアスや細い字形により、数値上は合格した文字が薄く見える場合があると説明しています。W3C:フォント描画の注意点 式の判定後も、実際の文字サイズとフォントで読みやすさとレイアウトを確認します。今回はページとの統合確認を実施しておらず、実験結果をブラウザー監査の結果とは呼べません。

7. 小数だけでなく、追跡できる判定を残す

冒頭の問いには明確に答えられます。4.50 は表示上の結果かもしれず、4.5:1 を満たすかは丸めていない計算値で決めます。sRGB の符号化を処理してから相対輝度と比を求め、文字の条件に合わせて閾値を選びます。最後に、入力が実際のページに対応し、デザイン案の別の背景を使っていないことを確認します。

有用な確認記録には、少なくとも前景、背景、元の比、表示値、閾値、判定を含め、不透明な単色の計算だけかも記します。同僚が結果を再現でき、未確認の範囲も把握できます。最低の閾値は、共通に検証できる根拠を提供します。計算手順と適用範囲を明確に保存することで、ページを変更した後もその根拠を使えるようになります。

自動ビルドに組み込むなら、プロジェクトで実際に使う色の組を一覧にし、どの組がどのテーマで失敗したかを計算処理から報告できます。色の名前、背景の名前、閾値を変えたときは、レビューで差分も示します。これは実装の提案です。スクリプトをいずれかのプロジェクトの継続的インテグレーションへ接続した実験はしていません。動的な状態と実際のフォントは別に確認する必要があり、一覧がすべて合格したからといって省略できません。