WCAG 对比度怎么算、怎么改到达标

对比度不是"看起来清不清楚"这种主观判断,WCAG 给了一个确定的公式,算出来是 1:1 到 21:1 之间的一个数。达标与否只看这个数是否跨过阈值。

第一步:算相对亮度

先把颜色拆成 R、G、B 三个 0–255 的通道值,对每个通道做归一化:

c = c8 / 255

若 c ≤ 0.03928:  c_lin = c / 12.92
否则:            c_lin = ((c + 0.055) / 1.055) ^ 2.4

L = 0.2126 × R_lin + 0.7152 × G_lin + 0.0722 × B_lin

那个 2.4 次幂是 sRGB 的伽马校正——屏幕上的颜色不是线性叠加的,必须还原到线性空间再加权。三个权重不一样是因为人眼对绿色最敏感,蓝色最不敏感。

顺带一提,WCAG 2.x 文本里的阈值写的是 0.03928,有些实现按 sRGB 规范取 0.04045。两者差异极小,不会影响任何达标的判定结果。

第二步:算对比度

对比度 = (L_较亮 + 0.05) / (L_较暗 + 0.05)

那个 +0.05 是为了避免纯黑(L=0)时出现除以零。取值范围是 1:1(完全相同)到 21:1(纯黑配纯白)。

写成代码:

function channel(c8) {
  const c = c8 / 255;
  return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
}

function luminance(r, g, b) {
  return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
}

function contrast(a, b) {
  const l1 = luminance(...a);
  const l2 = luminance(...b);
  return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
}

contrast([255, 255, 255], [118, 118, 118]);  // 4.54
contrast([255, 255, 255], [89, 89, 89]);     // 7.00

阈值表

内容类型 AA(最低要求) AAA(增强)
正文文本 4.5:1 7:1
大号文本 3:1 4.5:1
非文本(图标、输入框边框、图表线条) 3:1 无明确要求

"大号文本"的界定要记准:18pt 及以上(约 24px),或 14pt 及以上的粗体(约 18.66px)。16px 的常规字号不算大号,必须按 4.5:1 走。

非文本那条来自 WCAG 2.1 的 1.4.11,指的是"理解内容所必需的"图形——一个纯装饰的背景纹样不要求,但一个只有描边、没有文字的搜索图标边框必须达到 3:1。

几个常见配色的实测值

前景 背景 对比度 判定
#767676 灰 白 4.54:1 AA 正文刚好过线
#595959 灰 白 7.00:1 达到 AAA
#999999 灰 白 2.85:1 不达标,这是最常踩的坑
白 #007BFF 蓝 3.98:1 大号文本可用,正文不达标

最后一行值得注意:Bootstrap 那套经典蓝配白字,用作按钮大字号可以,用作 14px 的正文链接是不达标的。很多"看起来很清楚"的品牌色,实测都在 3.5–4.2 这个尴尬区间。

怎么改到达标

  1. 先确定改前景还是改背景。 改字色通常比改背景安全,因为背景色往往绑定了品牌。把 #999 压到 #767676 就能过 AA,压到 #595959 能过 AAA。
  2. 别靠调透明度。 给文字加 opacity: 0.6 后的实际颜色取决于它叠在什么上面,背景是图片或渐变时根本算不出来。要改就改成确定的色值。
  3. 渐变和图片背景上放文字时,取最亮处和最暗处分别验算,两处都要达标。稳妥做法是加一层半透明的纯色蒙版,把背景压成可控的单一亮度。
  4. 品牌色不达标时,保留品牌色做大色块和图标,正文用它加深后的变体(在 HSL 里降 L,而不是只降饱和度)。
/* 不推荐:实际颜色依赖背景,无法验算 */
.muted { color: #333; opacity: 0.5; }

/* 推荐:写成确定的色值,可以算、可以验 */
.muted { color: #767676; }

这些情况不用管

  • 禁用状态的控件(disabled)。WCAG 明确豁免。
  • 纯装饰性内容,去掉也不影响理解的图形。
  • Logo 和品牌名称中的文字,没有对比度要求。
  • 用户代理默认样式(未加任何 CSS 的原生控件)。

对比度之外还要做一件事

WCAG 1.4.1 要求"不能仅靠颜色传达信息"。表单报错只把输入框描边变红是不够的——色盲用户看不出区别。必须同时给出文字说明或图标。

最后提一句:APCA 是 WCAG 3 的候选对比度算法,对深色模式的判断比现有公式更合理,但它还不是正式规范,验收请继续按 4.5:1 这套来。

怎么在浏览器里检查

Chrome 和 Edge 的 DevTools 里,选中元素后在 Styles 面板点击颜色值旁边的小方块,会弹出取色器并直接显示该颜色与背景的对比度,以及是否达到 AA / AAA。这是最快的单点检查方式。

整页扫描用自动化工具:Lighthouse 的 Accessibility 分项和 axe DevTools 插件都能批量列出对比度不达标的元素,给出具体颜色值和实测比值。它们用的就是上面这套公式,所以和手算结果一致。

但工具只能检查静态且可计算的颜色。下面几种情况它们会跳过或报错,必须人工验算:

  • 文字叠在图片或渐变上;
  • 使用了半透明色(rgba() 或 opacity);
  • 颜色由 JS 动态计算或随主题切换;
  • 悬停、聚焦、激活这些交互态。

深色模式要单独验一遍

浅色模式下达标的配色,反转成深色模式不一定达标。常见情况是背景换成深灰后,原来的品牌色文字在深底上对比度掉到阈值以下——因为对比度取决于两者的相对亮度差,不是"一深一浅"就够了。

做法是给深色模式单独定义一套前景色,通常是在 HSL 里提高 L 分量(而不是提高饱和度),然后对每个组合重新验算一遍。

别忘了焦点样式

键盘用户靠焦点框定位,所以焦点指示器(focus ring)同样要求 3:1 的对比度。很多设计为了"干净"直接写 outline: none,这是明确的无障碍缺陷。正确做法是换成自定义的高对比度描边,而不是把它删掉。

自己动手试试

相关工具