写主题的时候,次要文字(卡片摘要、日期、页脚)我随手挑了个灰:#8b95a1。看着挺舒服,14.5px,和白底的关系也算柔和。
然后我把它代进 WCAG 的对比度公式算了一下:3.04 : 1。小字要求是 4.5 : 1。不合格。
公式不长,自己算比猜靠谱
def lum(hex_):
vs = [int(hex_.lstrip('#')[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
r, g, b = map(f, vs)
return 0.2126 * r + 0.7152 * g + 0.0722 * b
def contrast(a, b):
la, lb = sorted((lum(a), lum(b)), reverse=True)
return (la + 0.05) / (lb + 0.05)
三段:sRGB 归一化 → 线性化(分段函数,低频段是线性的)→ 按人眼敏感度加权(绿最高 0.7152,蓝最低 0.0722)。最后 (亮 + 0.05) / (暗 + 0.05)——加 0.05 是为了不让纯黑除以纯黑炸掉。
实测数字(真跑出来的,不是估计)
| 前景 | 背景 | 比值 | AA 正文 4.5 |
|---|---|---|---|
#a5aeb8(卡片日期原色) |
#ffffff | 2.25 : 1 | ✗ |
#8b95a1(次要文字原色) |
#ffffff | 3.04 : 1 | ✗ |
#8b95a1 |
#f6f7f9(页面底色) | 2.83 : 1 | ✗ |
#767f8a |
#ffffff | 4.06 : 1 | ✗ 差一点 |
#6b7280 |
#ffffff | 4.83 : 1 | ✓ |
#667085 |
#ffffff | 4.97 : 1 | ✓ |
#667085 |
#f6f7f9 | 4.64 : 1 | ✓ |
#0a84ff(系统蓝) |
#ffffff | 3.65 : 1 | ✗ |
#2563eb(现在用的链接色) |
#ffffff | 5.17 : 1 | ✓ |
#e6edf3(代码块正文) |
#0d1117 | 16.02 : 1 | ✓ |
#ffffff |
#0d1117 | 18.92 : 1 | ✓ |
三个结论,两处改动
一、同一色系里,”浅一点点”经常就直接掉下去了。 #767f8a 是 4.06,#6b7280 是 4.83。肉眼几乎分不出,规范上一个不合格一个合格。这类边界必须靠算,手感在这里没有分辨率。
二、”系统蓝”不能直接拿来当正文链接色。 #0a84ff 是 3.65 : 1。它做大按钮的底色、做图标都很好,但当 14–16px 的文字颜色就不合格。#2563eb 5.17,同样一眼蓝,能用。
三、改了变量,别只改一处。 我原来是 --c-muted 一个变量管所有次要文字,但卡片日期那行单独写死了 #a5aeb8(2.25 : 1,最差的一处)。所以两处一起改:变量 #8b95a1 → #667085,卡片日期改成引用同一个变量。
关于代码块:为什么不用纯白配纯黑
#ffffff 配 #0d1117 是 18.92 : 1,比现在用的 16.02 更”清楚”。但长段代码里,过高的对比会让眼睛更快疲劳——文字边缘的极端反差在长时间阅读时是负担。
16 : 1 已经远超 AA(4.5)和 AAA(7)的要求,留出来的那点余量是给”连续看二十分钟”准备的。这个取舍没有客观标准,我承认是主观的。
但有一件事不主观:先算,再取舍。凭感觉挑个灰,和算完之后决定”这里我故意用 4.0,因为它是纯装饰不承载信息”——这两件事看起来结果一样,实际上只有后者知道自己冒了什么风险。
一句给自己留的话
AA 的 4.5 是下限,不是目标。小字号(13–14px)我以后尽量做到 5 : 1 以上,因为抗锯齿和屏幕密度会让实际观感比公式算的更差——公式里没有这两样东西。