对比度 · 深色模式

为什么 WCAG 的对比度公式
在深色模式下不准

公式是对称的:把前景和背景对调,算出来的比值一模一样。 可人眼不是对称的。深底浅字比浅底深字难读得多。 差在哪、差多少,以及在新标准定稿之前该怎么办。

先看一个能自己动手验的例子。下面两块,左边是深字压白底,右边是浅字压黑底, 两边的 WCAG 比值永远相等,不是挑出来的巧合,是按比值反推出来的灰。 拖滑杆改比值,看看 APCA 那个数字怎么走。

同一个 WCAG 比值,两种极性

4.50:1

深色的字压在浅色的底上。

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 压 #ffffff4.54:15.5:1
#333333 压 #0000001.66:1
#1a1a1a 压 #0000001.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

角色色值WCAGAPCA白底上同比值的灰那个灰的 Lc
正文#e9eaf015.70:1Lc −93.8#232323Lc +102.7
次要文字#a9adbb8.42:1Lc −57.5#4d4d4dLc +89.1
弱化文字#868b9a5.54:1Lc −39.6#686868Lc +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 的结论是不一样的。这篇为了讲清楚差异做了简化。

另外,"深色模式更难读"讲的是可读性,不是舒适度。对畏光、偏头痛、 部分弱视的用户来说,深色界面明显更好受。这两件事不冲突,别拿这篇去论证"深色模式不该做"。

动手

文章里所有数字都是 配色工作台 那套引擎当场算的,页面上那个滑杆也是。你可以拿自己项目的颜色去跑一遍, 它会把浅色深色两套一起校,两个口径的数一起给, 打架的地方专门标出来。

接着看

汉字比拉丁字母更需要对比度 笔画密、竖笔细,同样的对比度下中文先糊。这件事中文站几乎没人认真讲过。 配色工作台 把这篇讲的东西直接用上:浅深双查、双口径、导出成对的 CSS 变量。