配了五年设计系统,每次想让文字在任何背景上都清晰可读,都要写一长串 luminance 计算——今天 CSS 把这件事彻底原生化了

写过五年设计系统,每次想让文字在任何背景上都清晰可读,都要先写一套 luminance 计算函数——一个函数算亮度、一个函数比较黑白、最后再套回去。上线之后还得担心用户换主题时 JS 还没跑起来那几百毫秒的 FOUC。

2026 年了,这件事被一个 CSS 函数从根上换了。

contrast-color() 是什么

Chrome 147、Firefox 146、Safari 26 全部稳定支持的 CSS 函数,CSS Color Level 5 规范,2026 年 4 月进入 Baseline。你给它一个颜色,它返回黑白两者中对比度更高的那个——全程浏览器在样式计算阶段完成,零 JS、零库、零构建步骤。

.button {
  background-color: var(--brand-color);
  color: contrast-color(var(--brand-color));
}

用户换一个主题色,文字色自动跟着变。没有 useEffect、没有 reflow、没有 hydration 时机问题。相当于把每个设计系统迟早会写的那段 luminance helper 永久删掉了。

三个典型场景

1. 设计系统动态主题色

.card {
  --bg: var(--card-color, #2d5a27);
  background-color: var(--bg);
  color: contrast-color(var(--bg));
}

不用再维护一套 --card-text-on-dark / --card-text-on-light 的变量组合。一个 --bg 走天下,深色背景自动出白字,浅色背景自动出黑字。

2. 深浅模式无缝切换

:root {
  --bg: #f5f5f5;
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #1a1a2e;
  }
}
body {
  background-color: var(--bg);
  color: contrast-color(var(--bg));
}

纯 CSS 实现,用户切系统主题不需要任何 JS 逻辑参与。

3. 悬停动态变色

.card {
  --bg: oklch(0.6 0.15 250);
  background: var(--bg);
  color: contrast-color(var(--bg));
}
.card:hover {
  --bg: oklch(from var(--bg) calc(l + 0.12) c h);
}

配合相对颜色语法,悬停时背景亮度提升,文字色自动从白切换成黑,零 JS。

一个必须知道的坑:中灰色陷阱

contrast-color() 只返回黑或白,目标 WCAG AA(4.5:1)。当背景是中等明度的颜色(#767676 附近)时,黑白两个选项都在阈值边缘徘徊,结果对用户实际可读性很差。

解决思路:把设计系统约束在明度高或低的范围,不要让主色调经过中灰区间。配合 oklch 的 lightness clamp 可以低成本做到:

.badge {
  /* 把输入推到已验证的亮度范围再对比 */
  --safe: oklch(from var(--badge-color) clamp(0.2, l, 0.45) c h);
  background: var(--safe);
  color: contrast-color(var(--safe));
}

渐进增强写法

.button {
  background-color: var(--brand);
  color: #fff; /* 老浏览器兜底 */
}
@supports (color: contrast-color(red)) {
  .button {
    color: contrast-color(var(--brand));
  }
}

不支持的浏览器拿到白字(需要人工确认白字在你的深色 band 里可读),现代浏览器拿到计算值。这是标准渐进增强,不是 polyfill——因为 contrast-color(var(--x)) 在构建时无法求值,必须运行时在浏览器里算。

和 JS 颜色库的真实对比

contrast-color() chroma-js polished
体积 0 kB ~14 kB ~11 kB
计算时机 样式计算时 JS 运行时 JS 运行时
依赖 需安装 需安装
配合 CSS 变量 ✅ 原生 需传值下去 需传值下去
暗色模式 一个声明 需要两套变量 需要两套变量

三步下一步

  1. 找你的 luminance helper:项目里搜 relative luminance / getLuminance / readableColor,如果搜到了,说明你已经需要这个函数了
  2. 迁移按钮/标签/卡片:把 color: var(--text-on-${variant}) 换成 color: contrast-color(var(--bg)),配合 --bg 变量走天下
  3. 约束你的色板:确认现有品牌色的 oklch lightness 落在 l < 0.25l > 0.45 区间,不在这个区间的颜色不要进主题色变量

WebAIM 统计了五年,83.9% 的页面仍然对比度不及格。工具没少出,数字几乎没动过。问题不在工具,在于这个计算从来就不该在 JS 层做——现在浏览器把这件事接过去了。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 12 阅读