先看一个能自己动手验的例子。下面两块,左边是深字压白底,右边是浅字压黑底, 两边的 WCAG 比值永远相等,不是挑出来的巧合,是按比值反推出来的灰。 拖滑杆改比值,看看 APCA 那个数字怎么走。
同一个 WCAG 比值,两种极性
深色的字压在浅色的底上。
Dark text on a light background.
12px 的小字长这样。
浅色的字压在深色的底上。
Light text on a dark background.
12px 的小字长这样。
两边的 WCAG 数字始终一致,APCA 的 Lc 却能差出两倍。滑到 4.5 那一档看看, 规范说这两个都"正文可用"。
停在 4.5 的时候,左边是 Lc 71,右边只有 Lc 29。 按 APCA 的分档,前者勉强够正文,后者连大字都不够,只配当装饰。 同一个数字,规范给了同一个结论,实际差了整整三档。
公式长什么样
WCAG 2.0 定下来、2.1 和 2.2 一直沿用的那条式子是这样的:
对比度 = (L1 + 0.05) / (L2 + 0.05)
L1 = 较亮那个颜色的相对亮度
L2 = 较暗那个颜色的相对亮度
注意"较亮"和"较暗":式子里根本没有"前景"和"背景"的位置。 这就是它对称的原因:交换两个颜色,分子分母不变。
相对亮度本身是把 sRGB 三个通道解 gamma 之后按
0.2126R + 0.7152G + 0.0722B 加权,
这一步是有物理依据的,对应人眼对红绿蓝的敏感度差异,没什么问题。
问题出在外面那层。
那个 0.05 是什么
它是环境光补偿。真实的屏幕不是理想黑体:室内光打上去有反射, 面板本身也有漏光,所以"纯黑"实际发出的光并不是零。规范拿 0.05 当作这个底噪的经验值,也就是白色亮度的 5%。
有它和没它,差别在暗端是灾难性的:
| 组合 | 带 0.05 | 不带 0.05 |
|---|---|---|
| #767676 压 #ffffff | 4.54:1 | 5.5:1 |
| #333333 压 #000000 | 1.66:1 | ∞ |
| #1a1a1a 压 #000000 | 1.21:1 | ∞ |
纯黑的相对亮度是 0,任何颜色除以它都是无穷大。所以这个常数是必需的, 它把暗端从"除零"救了回来。但代价是:它同时把暗端的差异整个压平了。 在 0.05 这个量级上,0.01 和 0.03 的亮度差被 +0.05 一淹,几乎没有区别; 可在真实的深色界面里,这两个值是"看得见"和"看不见"的分界。
这条公式是为"浅底深字"这个默认场景调的。深色模式在它定稿的年代还不是主流。
人眼为什么不对称
光学上有个现象叫光渗(halation,也叫 irradiation): 亮的区域会在视网膜上向暗处溢出一点,让亮的部分显得比实际更胖。 浅字压深底的时候,笔画会往外"发光",笔画之间的缝隙被吃掉; 反过来深字压浅底,笔画只会显得更瘦,缝隙反而被撑开。
这个效应对细笔画、小字号最狠,对有散光的人尤其明显: 散光本来就让点光源拖尾,深底亮字会糊成一片。
还有个更朴素的原因:瞳孔。看深色界面时瞳孔放大, 进光量增加,同时景深变浅、像差变大,对细节的分辨能力下降。 这些都是 WCAG 那条式子里没有的变量。
APCA 怎么算
APCA 是 WCAG 3 起草过程中做出来的替代算法,全称 Accessible Perceptual Contrast Algorithm。它跟老公式最大的不同就一句话:它知道谁是字、谁是底。
// 深字压浅底
S = (Ybg^0.56 - Ytxt^0.57) × 1.14
// 浅字压深底,换了一组指数
S = (Ybg^0.65 - Ytxt^0.62) × 1.14
两组不同的指数,就是把上面那些不对称塞进模型里的方式。 再加上贴近纯黑那一段的软钳位(避免 gamma 把微小差值放大到失真), 最后输出一个叫 Lc 的带符号数:正数是深字压浅底,负数是浅字压深底, 绝对值越大越清楚。
APCA 给的参考分档大致是这样,实际还要按字号字重查表:
| Lc | 够干什么 |
|---|---|
| 90+ | 小字正文都行 |
| 75+ | 正文可用 |
| 60+ | 次要文字 |
| 45+ | 只够大字 |
| 30+ | 只配当装饰 |
差多少:拿真实的深色板算一遍
合成的灰阶说服力有限。下面这套是
azi36.com
自己在用的深色板,底色 #101116:
| 角色 | 色值 | WCAG | APCA | 白底上同比值的灰 | 那个灰的 Lc |
|---|---|---|---|---|---|
| 正文 | #e9eaf0 | 15.70:1 | Lc −93.8 | #232323 | Lc +102.7 |
| 次要文字 | #a9adbb | 8.42:1 | Lc −57.5 | #4d4d4d | Lc +89.1 |
| 弱化文字 | #868b9a | 5.54:1 | Lc −39.6 | #686868 | Lc +77.9 |
看最后一行。#868b9a 在深色底上是 5.54:1,WCAG AA 稳稳通过,
按规范它可以放正文。但 APCA 只给了 Lc 39.6,连"次要文字"那一档都够不上,
只够放大字。而白底上同样 5.5:1 的那个灰,APCA 给到 77.9,是正常的正文级。
同一个比值,一个 Lc 78,一个 Lc 40。 这不是理论差异,这是很多深色界面"数值全绿、看着就是费劲"的原因。
那该怎么办
三条,按重要性排:
一、两个数一起看,别只看一个
WCAG 2.1 是现行规范。无障碍验收、政府采购、 欧盟 EN 301 549、美国 Section 508,认的都是它。 APCA 至今仍是草案,WCAG 3 没有定稿时间表。 所以不能拿 APCA 去反驳验收,也不能因为 APCA 过了就不管 WCAG。
实际做法是:WCAG 当下限,APCA 当参考。两个都过最好; 只过一个的时候,把那一组颜色亲眼看一遍再决定。分歧的地方最值得看。
二、深色模式的字,比你以为的还要再提一档
如果你的浅色正文是 4.5:1 起步,深色那边照抄 4.5:1 会明显偏虚。 按上面那张表的经验,深色底上想拿到跟浅色相当的观感, 比值大致要翻一倍上去。或者干脆直接盯 Lc,别看比值。
三、底色别用纯黑
纯黑 #000000 配浅色字,光渗最严重,也最容易让 WCAG 给出虚高的数字
(因为分母被 0.05 兜底了)。用 #101116 这种"很深但不是黑"的底,
光渗轻一些,暗端的层次也还留得住。纯黑之下没有更深的颜色可以拿来做层次。
这篇的边界在哪
APCA 的分档(90/75/60/45/30)是参考值不是通过线:它真正的用法要按字号和字重查表, 同样的 Lc,14px Regular 和 24px Bold 的结论是不一样的。这篇为了讲清楚差异做了简化。
另外,"深色模式更难读"讲的是可读性,不是舒适度。对畏光、偏头痛、 部分弱视的用户来说,深色界面明显更好受。这两件事不冲突,别拿这篇去论证"深色模式不该做"。
动手
文章里所有数字都是 配色工作台 那套引擎当场算的,页面上那个滑杆也是。你可以拿自己项目的颜色去跑一遍, 它会把浅色深色两套一起校,两个口径的数一起给, 打架的地方专门标出来。