配色を「センス」の問題として扱うと、レビューで議論が終わりません。コントラスト比という数字を先に置くと、少なくとも読めるかどうかは客観的に決まります。
この記事では、実際にこのブログのパレットを全部測りました。結果として3組が基準を下回りました。そのうえで「落ちたから直す」で終わらせず、どう判断したかまで書きます。
まず測る
このブログの色は、CSSカスタムプロパティで名前をつけて管理しています。
:root {
--tb-ink: #0e1526; /* 深藍: ヘッダー・本文テキスト */
--tb-paper: #f3f6fa; /* 方眼紙の地 */
--tb-node: #ffffff; /* カード地 */
--tb-line: #ccd7e8; /* 罫線 */
--tb-violet: #6d5ae6; /* リンク・アクセント */
--tb-teal: #14b8a6; /* ラベル・状態表示 */
--tb-slate: #5a6b85; /* 補助テキスト */
}
実際に組み合わせて使われている12通りを、WCAG 2.1 の相対輝度の式で計算しました。

基準は2つあります。
- 4.5:1
本文サイズの文字に必要(WCAG AA) - 3:1
大きい文字(18pt以上、または14pt以上の太字)とUI部品に必要
落ちた3つをどう判断したか
ここからが本題です。数字が基準を下回ったからといって、自動的に直すべきとは限りません。WCAGの基準は「その色が意味を運んでいるか」で適用範囲が変わります。
ティール #14b8a6(2.49:1)→ 直さない
白地で2.49:1。本文に使ったら完全にアウトの数字です。実際にどこで使われているかCSSを見返したところ、全部これでした。
.l-sidebar .c-widget__title::before { content: "## "; color: var(--tb-teal); }
.wp-block-categories-list .cat-item a::before { content: "├── "; color: var(--tb-teal); }
.p-toc__ttl::before { content: "/* "; color: var(--tb-teal); }
::before で入れている ## や ├── という装飾記号だけでした。Markdownやツリー表示の見た目を借りた飾りで、これが読めなくても情報は一切失われません。見出しの文字自体は --tb-ink で18.20:1です。
WCAGは純粋な装飾には コントラスト基準を適用しません。なので、ここは基準を満たしていない状態で正しい、という判断になります。
ただし注意点があります。「装飾だから」で通せるのは、その要素を消しても意味が変わらない場合だけです。もしこのティールが「必須」「エラー」といった状態を色だけで示していたら、装飾ではなく情報なので直す必要があります。
ダーク面のバイオレット(3.69:1)→ 使う場所を限定する
深藍の上にアクセント色を置くと3.69:1。本文には足りないが、大きい文字とUI部品の基準3:1は満たしています。
この場合の選択肢は2つで、色を明るくするか、使う場所を大きい文字に限定するかです。このサイトではダーク面に明るいティール #3ddbc0(10.48:1)を別に用意していたので、暗い背景の上ではそちらを使うという役割分担にしました。
ここで大事なのは、1つの色が全部の背景で使えるわけではないということです。「アクセントカラーは1つ」と決めると、必ずどこかで破綻します。
罫線 #ccd7e8(1.45:1)→ 用途による
区切り線のような純粋に装飾的な境界線には基準は適用されません。一方で、入力欄の枠線のように「ここが操作できる領域だ」と示している線は UI 部品なので3:1が必要です。同じ色でも、役割によって判定が変わります。
いちばん危ないのは「ギリギリ通った色」
実は今回の測定でいちばん気になったのは、落ちた色ではありません。
| 組み合わせ | 比 | 基準 | 余裕 |
|---|---|---|---|
| リンク色 / カード地 | 4.93:1 | 4.5:1 | +0.43 |
| リンク色 / 方眼紙の地 | 4.55:1 | 4.5:1 | +0.05 |
同じリンク色でも、背景が白か薄い灰色かで数字が変わります。方眼紙の地の上では4.55:1で、基準まで0.05しかありません。
この状態の何が危ないかというと、今後の変更で簡単に割れることです。背景をほんの少し明るくする、リンク色を少しだけ鮮やかにする、といった「見た目の微調整」で基準を下回ります。そして下回ったことに誰も気づきません。見た目はほとんど変わらないからです。
だから、パレットを決めるときは基準ちょうどではなく、余裕を持った数字を狙います。目安として、本文系は7:1、リンクなどのアクセントは5.5:1くらいを取っておくと、後の調整で割れにくくなります。

色だけで情報を伝えない
コントラスト比は「読めるか」の基準であって、「区別できるか」の基準ではありません。ここは別の問題として扱う必要があります。
日本人男性の20人に1人程度は色覚特性を持つとされ、赤と緑の判別が難しい場合があります。コントラスト比を満たしていても、赤と緑で成功・失敗を表していれば区別できません。
- 色 + 記号
エラーに「⚠」、成功に「✓」を添える - 色 + 文字
グラフの凡例ではなく、系列そのものにラベルを置く - 色 + 位置や形
同じ形の色違いにしない
この記事のグラフも、通過/不通過を色だけでなく「AA 通過」「本文には使えない」というテキストで併記しています。グレースケールで印刷しても読めるかどうかが簡単な確認方法です。
忘れられがちな2つ|フォーカスと「大きい文字」の定義
フォーカスリングにも基準がある
本文の色ばかり気にしていると抜けるのが、キーボード操作のときに出る枠線です。WCAG 2.1 では、フォーカス表示も隣接する色に対して3:1が必要と定められています。
:where(a, button, input, summary):focus-visible {
outline: 2px solid var(--tb-violet);
outline-offset: 2px;
}
このサイトはアクセント色をそのまま使っているので、白地で4.93:1、方眼紙の地で4.55:1。基準の3:1は満たしています。
ここで避けたいのが outline: none です。「枠線が出るのが格好悪い」という理由で消すと、キーボードだけで操作している人はいま自分がどこにいるか分からなくなります。デザインが気になるなら、消すのではなく :focus-visible を使って「マウス操作のときは出ない」ようにします。上の :where() は詳細度を0にするための書き方で、あとから個別に上書きしやすくなります。
outline-offset を少し取っているのにも意味があります。要素の色と枠線が接していると、背景次第で境界が見えにくくなるためです。
「大きい文字」は何pxからか
基準が4.5:1から3:1に緩むのは「大きい文字」ですが、この定義がptで書かれているため、CSSでいくつなのかが分かりにくくなっています。
| 条件 | WCAGの定義 | CSSでの目安 |
|---|---|---|
| 通常の太さ | 18pt 以上 | 24px 以上 |
| 太字 | 14pt 以上 | 18.66px 以上 |
つまり20pxの見出しは「大きい文字」ではありません。感覚的には十分大きく見えるので、ここは間違えやすいところです。3:1で通そうとしている色を見出しに使う場合は、実際のフォントサイズを確認する必要があります。
さらに日本語の場合、同じpx数でも書体によって線の太さがかなり違います。基準は満たしていても細いフォントだと読みにくい、ということが普通に起きます。数字はあくまで下限で、実際の見え方は別に確認する必要があります。
破綻しないパレットの作り方
今回測ってみて、事故を防いでいたのは色選びのセンスではなく構造のほうでした。
色に「役割」の名前をつける
--blue-500 ではなく --tb-slate(補助テキスト) のように役割で名前をつけると、使う場所が名前で決まります。「補助テキスト」という名前の色を見出しに使おうとは思いません。
逆に色名で管理していると、「なんとなく合う」という理由で本来と違う場所に使われ、気づいたら同じ色が5つの意味を持っています。
背景の種類を先に決める
今回の測定で分かったとおり、コントラスト比は前景と背景のペアで決まります。背景が3種類あれば、前景色ごとに3回測る必要があります。
背景を「カード地・紙面・ダーク面」の3つに限定しておくと、確認すべき組み合わせが有限になります。背景色が場当たり的に増えていくと、そもそも全部を測ることができなくなります。
測る作業をコードにしておく
相対輝度の計算式は短いので、スクリプトにしておけば毎回すぐ測れます。
def luminance(hex_color):
h = hex_color.lstrip('#')
r, g, b = [int(h[i:i+2], 16) / 255 for i in (0, 2, 4)]
f = lambda c: c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b)
def contrast(a, b):
l1, l2 = sorted([luminance(a), luminance(b)], reverse=True)
return (l1 + 0.05) / (l2 + 0.05)
ブラウザの検証ツールでも1組ずつ測れますが、パレット全体を一覧にできることに価値があります。今回「リンク色の余裕が0.05しかない」ことに気づけたのは、全組み合わせを並べたからでした。1組ずつ見ていたら「4.55、基準を満たしている」で終わっていたはずです。
まとめ
- まず全組み合わせを測る
1組ずつでは「ギリギリ通っている色」に気づけない - 基準を下回っても、装飾なら直さなくていい
判断基準は「その色が意味を運んでいるか」 - 1つの色が全部の背景で使えるとは限らない
暗い面用の色は別に用意する - 基準ちょうどを狙わない
余裕がないと、後の微調整で気づかないうちに割れる - 色だけで情報を伝えない
コントラスト比は「区別できるか」の保証ではない - 色には役割で名前をつける
名前が使う場所を決めてくれる
次に読む記事



