#999999 放在白底上为什么看不清?对比度怎么改到 4.5:1

设计稿挑了一档灰做次要文字,肉眼「挺淡」,审计却判不过 AA——症结在相对亮度不在色号。本文讲清 WCAG 对比度的计算式、AA 与 AAA 四个阈值分别管什么、半透明色叠加后对比度为什么反而更差,以及不改设计稿也能把文字改到 4.5:1 的四步做法。

约 7 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证

#999999 配白底只有 2.85:1,问题出在相对亮度不在色号

典型症状是:设计系统里挑了一档灰做次要文字,视觉评审没人反对,审计工具直接判不过 AA。症结在于 WCAG 从不比较色号的深浅——它先把每个通道的 8 位数值做伽马展开,再按 0.2126 / 0.7152 / 0.0722 加权成一个 0 到 1 的相对亮度 L,最后比 (L1 + 0.05) / (L2 + 0.05)。所以 #999999 的问题不是「灰得不够狠」,而是它的 L 离纯白太近;同一档灰挪到黑底上,对比度立刻跳到 20:1 以上。

可直接跑的对比度函数(浏览器控制台 / Node 均可)
const srgb = c => { c /= 255; return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; };
const lum = hex => {
  const n = parseInt(hex.replace("#", ""), 16);
  return 0.2126 * srgb(n >> 16) + 0.7152 * srgb((n >> 8) & 255) + 0.0722 * srgb(n & 255);
};
const ratio = (a, b) => {
  const [hi, lo] = [lum(a), lum(b)].sort((x, y) => y - x);
  return (hi + 0.05) / (lo + 0.05);
};

ratio("#999999", "#ffffff");  // 2.85  AA 不通过
ratio("#767676", "#ffffff");  // 4.54  AA 通过
ratio("#4f8cff", "#000000");  // 6.53  AA 通过,差一点到 AAA

四个阈值:AA 的 4.5、AAA 的 7、大字放宽到 3 与 4.5

正文的标准是 4.5:1,AAA 要求 7:1;字号达到 24px,或 19px 且加粗时,标准放宽到 3:1,AAA 对应 4.5:1。别把这条规则简化成「一律 4.5」——非文本元素适用的是另一条线:图标、输入框边框、图表里相邻的色块、焦点圈,只要能识别就需要 3:1。一个用 #e5e7eb 画在白底上的输入框边框只有 1.24:1,等于没画。

症状 → 真实比值 → 修法:照着改,不用逐个试

下面六个组合是实际项目里最常见的失败点。比值都按上文公式算过,改法只动一个变量,照抄即可。

症状真实比值修法
正文灰 #999999 配白底2.85:1,AA 不通过压到 #767676(4.54:1),或直接换更深的灰阶
灰底卡片里用 #9ca3af2.31:1(#9ca3af on #f3f4f6)背景比文字还浅时把文字压深,别只调背景
品牌蓝 #4f8cff 做按钮底配白字3.22:1,未过 AA底色压深到 #4173d1(4.58:1),色相不变
白字压在 50% 透明蓝上合成后 1.73:1,比原色更差先与背景合成再算,别拿原色值直接判
图标 / 输入框边框用 #e5e7eb1.24:1非文本按 3:1,把边框色改成 #9ca3af 以下更深
深色主题沿用浅色主题的灰#94a3b8 on #0f172a = 6.96:1(通过)两套主题各算一遍,别假设「浅色过了深色就过」

透明度是最容易算错的一环:50% 蓝底白字只剩 1.73:1

rgba(79,140,255,0.5) 这类半透明色不能直接拿去算对比度,因为 WCAG 比的是「最终显示出来的那个颜色」。正确做法是先按 alpha 把前景与背景合成成不透明色再算:#4f8cff 以 50% 叠在白底上合成出 #a7c6ff,对比度只有 1.73:1,比不透明时的 3.22:1 更差——透明度不会救场,只会让问题更隐蔽。毛玻璃、遮罩层、hover 态的半透明底色都要过这一步;毛玻璃还要按最坏背景评估,因为背后的图不可控。

合成后再判定:半透明底色必须走这一步
const over = (fg, bg, alpha) => {
  const p = h => [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)];
  const [f, b] = [p(fg), p(bg)];
  return "#" + f.map((v, i) =>
    Math.round(v * alpha + b[i] * (1 - alpha)).toString(16).padStart(2, "0")).join("");
};

over("#4f8cff", "#ffffff", 0.5);  // → "#a7c6ff"
ratio("#a7c6ff", "#ffffff");       // → 1.73  白字压在上面不可读
批量扫色板:一次列出所有不达标的组合
const pairs = [["#6b7280","#ffffff"],["#9ca3af","#ffffff"],["#6b7280","#f3f4f6"],["#4f8cff","#ffffff"]];
pairs.forEach(([fg, bg]) => {
  const r = ratio(fg, bg);
  console.log(`${fg} on ${bg} = ${r.toFixed(2)}  ${r >= 4.5 ? "AA ok" : "fail"}`);
});
// #6b7280 on #ffffff = 4.83  AA ok
// #9ca3af on #ffffff = 2.54  fail
// #6b7280 on #f3f4f6 = 4.39  fail ← 同一个色,换个底色就掉线
// #4f8cff on #ffffff = 3.22  fail

不改设计稿也能过 AA:四步改色,按顺序试

  1. 先量现状:把前景与背景都换成代码里最终生效的值跑一次。设计稿上的色和代码里的色经常不是同一个,拿稿面的值去判定会得出错误结论。
  2. 决定改哪一头:文字多、背景少的页面改文字(压深文字),反过来就改背景。一屏十几个文本色时,压深一次主题背景能一劳永逸。
  3. 锁色相、只动亮度:保持 H 与 S 不变,按 2% 到 3% 步长压亮度直到跨过 4.5。#4f8cff 压到 #4173d1 就是 4.58:1,色相没变,观感仍是同一个蓝。
  4. 大字号走 3:1:标题、24px 以上的价格数字、图表主标注可以按 3:1 判定,省下的视觉空间留给真正的正文。
  5. 把结果锁进 token 并复查四态:hover / focus / active / disabled 常常只设计了一个值,另外几态上线前要单独算一遍。

对比度合格不等于可访问:还有三件事工具算不了

对比度只解决「看得清」,不解决「看得懂」。仍需人工确认的有三类:色觉障碍用户能否区分状态色——红与绿表示错误与成功在 deuteranopia 下几乎同色;只靠颜色传达的信息——红点、彩色标签、图表里靠颜色区别的两条线,要补图标或文字;以及深色模式必须在真实背景上重算一遍,从浅色主题继承来的半透明层在深底上基本都失效。工具替代的是算术,判断替代不了。

上线前三条自查:只测默认态等于没测

第一,默认态过了还要跑 hover / focus / active / disabled,多数主题只在前两个上达标。第二,深色模式单独跑一遍,不要假设「浅色过了深色就过」——阈值不变但 L 值全变了。第三,图片上的文字与图标,背景是像素不是纯色:取色后按背景最亮与最暗两个极值算,区间内只要有一个不达标就该加半透明遮罩或改用图片打码区域来压底色。

常见问题

WCAG 的对比度 4.5:1 到底是怎么算出来的?

先看结论:把前景与背景的每个 RGB 通道做伽马展开,按 0.2126 / 0.7152 / 0.0722 加权得到相对亮度,再用 (L1 + 0.05) / (L2 + 0.05)。手算远不如工具快——两个色值贴进 WCAG 对比度检测,直接读比值和 AA / AAA 判定,顺带看它给出的修正建议色。

半透明背景的颜色怎么算对比度?

不能直接算,必须先合成。rgba 色与背后颜色按 alpha 混合后才形成实际显示色,把合成结果当成普通不透明色再算比值。例如 #4f8cff 叠 50% 在白底上得到 #a7c6ff,对比度只有 1.73:1。毛玻璃遮罩背后内容不可控时,按最亮的背景算才是安全做法。

大字号用 3:1 就够了吗?

够,但要先确认字号真的达标:24px,或 19px 且加粗。中文正文的实际显示高度比数字小,同字号下笔画更密,通常从严按 4.5:1 处理。此外 3:1 这条线专属于非文本元素(图标、边框、焦点圈),不要拿它去省正文的对比度。

对比度全部达标了,能说通过 WCAG 了吗?

不能,达标只是文本可读性这一项。还要满足键盘可操作、焦点可见、不只靠颜色传达信息(色觉障碍)、以及图片文本有替代方案。工具能替你算数值,替代不了这四类判断。

📂

松开以自动格式化

支持 .json / .sql / .csv / .html / .css / .xml / .yaml 文件