我曾经写过一个对比度检测工具,用来验证网站的配色是否符合 WCAG。它跑起来很顺畅,输出很漂亮,退出码很规矩。
然后它骗了我。
它做了什么
它的逻辑很简单:从页面上取出每个元素的计算样式,拿到前景色和背景色,算相对亮度,套上对比度公式。听起来没有余地出错。
问题出在一行看起来无害的代码上:
const fg = "rgba(255, 255, 255, .28)";
它把这个值当成了不透明白色,于是得到白字压在黑底上的 16.55:1——完美。
但 28% 的白叠在近黑的底上,真实颜色大约是对应的 rgb(96,94,92)。算出来是 2.53:1。
差了六倍多。而这一项在那个版本里是「通过」。
为什么没被发现
因为它符合预期。工具告诉我这一项很好,我没有理由不信;页面看起来也能读,我也没有理由去怀疑数字。
一个总是说「通过」的检测器,和一个坏掉的检测器,从外面看是一样的。
这是我后来才想明白的事:验证工具的价值不在于它能跑,而在于它在该报错的时候会报错。而证明这一点,唯一的办法是给它喂一个已知必定失败的样本。
修法
alpha 不能直接当作颜色用,它要和背景逐层合成:
function composite(fg, bg) {
const a = fg.a;
return {
r: fg.r * a + bg.r * (1 - a),
g: fg.g * a + bg.g * (1 - a),
b: fg.b * a + bg.b * (1 - a),
};
}
背景本身也可能是半透明的,所以合成要从最底层往上走,而不是只合一次。修完之后,那个 2.53:1 被如实报了出来,同一轮还牵出了另外五个真正的缺陷。
这一轮抓到的六件事
- 一个 CSS 变量在同一个块里被定义了两次,后者悄悄把 ink 档从 5.36 降到 4.54。
- chip 的 ok 状态用了主档绿色,4.29:1,应该用 ink 档。
- 进度条的 warn 状态用了琥珀,3.18:1,同样该换档。
- 一个被全仓二十多处当小字用的变量,4.32:1,差一点点。
- 夜读模式下输入区提示 1.11:1,几乎不可见——深色块借了「墨/纸」的语义,夜读时「墨」是浅的,块翻成了浅底,字还是浅的。
- 夜读块漏了一个变量,继承了日间的深绿,黑底上 3.24:1。
其中 5 和 6 是同一类错误:只给日间主题的值,指望另一个主题自动正确。 这类错误不会在明亮环境里出现,它只在用户切到暗色的那一刻出现——而如果你只截白天的图,你永远看不到它。
结论
- 工具的输出要能被证伪。先给它一个必定失败的用例。
- 每个当文字用的令牌,两个主题各给一个值。不许继承。
- 深色块用专用令牌,不要靠语义反转。
- 验证要覆盖真正会出问题的状态,而不只是默认状态。
我觉得这已经不只是配色的事。任何自动化检查,如果从没失败过,第一件事应该是怀疑它。








