现在的网页动不动就要搞个动态背景,让文字颜色自动适配背景深浅。这活儿看着简单,真上手就发现全是坑。什么WCAG对比度公式,什么APCA新标准,光是一堆数学公式就能把人绕晕。但咱前端打工人不能怂,今天就把这问题给盘明白。
面对一个可以随意变换的背景色,怎么用纯CSS代码让文字在深色背景上自动变白,浅色背景上自动变黑?直接上硬核方案:利用OKLCH色彩空间的感知亮度特性,通过一行
oklch(from ... round(1.21 - L) 0 0)计算公式,就能实现智能切换。这招比折腾WCAG那一大堆RGB换算要清爽得多,而且更贴合最新的APCA视觉对比标准。文章里会手把手拆解这串代码的来历,顺便教一招还能把黑色换成任意主题色的骚操作。
WCAG 与 APCA
WCAG 2.2 里头的对比度算法,本质上是拿两个RGB颜色的亮度值做除法。亮度计算得先把RGB数值经过一堆2.4次幂的复杂变换,最终得到一个0到1之间的亮度值。然后套用公式 (L1+0.05)/(L2+0.05),算出来的比值就是对比度。这个算法虽然严谨,但写成CSS代码得堆出一长串pow函数,可读性基本为零。
APCA 是下一代对比度标准,更贴近人眼对颜色亮度的真实感知。它不再用简单的亮度除法,而是引入了更复杂的数学模型。这玩意儿虽然更准,但要是硬写成CSS,代码复杂度直接起飞,维护起来能让人头秃。
OKLCH 色彩空间
OKLCH 是个好东西,它的L通道(感知亮度)数值直接反映了颜色在肉眼看来有多亮。这个数值从0到1,0是最暗,1是最亮。为啥要请它出场?因为有了这个现成的亮度值,就不用自己吭哧吭哧去算RGB的亮度了,直接拿来用,省了老鼻子劲儿。
举个例子,纯红色在OKLCH里头的L值大概是0.6左右,纯黄色能达到0.9。这意味着黄色背景上黑字比白字更清楚,红色背景上白字更占优。
找到黑白切换点
得先搞清楚一个事儿:L值到多少的时候,白色文字就比黑色文字更清楚?直觉可能觉得是0.5,但实测发现根本不是那回事儿。很多颜色就算L值到了0.7,黑色文字依然糊成一团。
搞了个小测试,用APCA标准去测不同L值下黑白文字的对比度。测试结果挺有意思:
- L值在0.72以上的颜色,黑色文字的APCA对比度稳稳超过白色文字
- L值在0.65以下的颜色,白色文字全面占优
- 中间那一段0.65到0.72,黑白对比度差距不大,都能看清
所以阈值就定在0.72,L值大于0.72用黑色,小于等于0.72用白色。
核心公式推导
目标很明确:把L值大于0.72的变成黑色,小于等于0.72的变成白色。在CSS里用 round() 函数就能实现这个开关效果。
round(1.21 - L, 0) 这串代码干的事儿:先算1.21减去L值。如果L是0.72,1.21减0.72等于0.49,四舍五入取整后是0。如果L是0.71,1.21减0.71等于0.5,四舍五入后是1。
最终 round() 输出的是0或1。把0和1作为亮度值塞进 oklch() 里:
- 当结果是0时,生成的颜色是
oklch(0 0 0),也就是黑色 - 当结果是1时,生成的颜色是
oklch(1 0 0),也就是白色
完整的写法就是这样:
color: oklch(from var(--bg-color) round(1.21 - L) 0 0);这行代码的意思:从背景色里提取L值,经过上面那串计算,得出0或1作为新的亮度,色度和色调都归零,最终输出纯黑或纯白。
实战操作流程
第一步:准备好背景色
背景色得是个CSS颜色变量,方便动态切换。比如:
.element {
--bg-color: #ff6b6b; /* 随便来个珊瑚橙 */
background-color: var(--bg-color);
}第二步:套上文字颜色
直接用刚才的公式设置文字颜色:
.element {
--bg-color: #ff6b6b;
background-color: var(--bg-color);
color: oklch(from var(--bg-color) round(1.21 - L) 0 0);
}这样文字颜色就会跟着背景色走。背景深的时候L值低,算出结果是1,文字变白;背景浅的时候L值高,算出结果是0,文字变黑。
第三步:验证效果
搞几个不同的背景色试试:
#000000纯黑,L值是0,公式算出1.21减0等于1.21,四舍五入取整得1,文字是白色#ffffff纯白,L值是1,公式算出1.21减1等于0.21,四舍五入得0,文字是黑色#ff6b6b珊瑚橙,在OKLCH里L值约0.7,1.21减0.7得0.51,四舍五入得1,文字是白色
第四步:应对边界情况
有些背景色的L值刚好卡在0.72附近,比如 #c0c0c0 银色。这种情况下黑白文字对比度都差不多,公式给哪个都能接受。如果觉得某个颜色不理想,可以微调公式里的1.21这个阈值参数。把数值调大一点,黑色文字出现的范围就更广;调小一点,白色文字出现的范围就更广。比如改成1.18,那L值大于0.68的就会变成黑色,更激进地倾向使用黑字。
高级玩法:切换主题色
黑和白可能不够用,有时候需要让文字在浅色背景上变成某种主题色(比如品牌蓝),深色背景上还是保持白色。这得动点脑筋,用 color-mix() 配合一些数学运算来实现。
核心思路:先用刚才的公式生成一个开关信号(0或1)。0代表应该用主题色,1代表应该用白色。然后用这个信号去混合主题色和白色。
具体步骤这样写:
.element {
--bg-color: #ff6b6b;
--base-color: #1e88e5; /* 主题蓝 */
--switch: oklch(from var(--bg-color) round(1.21 - L) 0 0);
color: rgb(from color-mix(in srgb, var(--switch), var(--base-color)) calc(2 * r) calc(2 * g) calc(2 * b));
}这串代码看起来有点绕,其实逻辑很直接:
color-mix(in srgb, var(--switch), var(--base-color)) 这一步把开关信号和主题色混在一起。开关信号是白色时(值为oklch(1 0 0)),混合结果等于白色和主题色的平均,也就是RGB各通道取半。开关信号是黑色时(值为oklch(0 0 0)),混合结果就是主题色本身(因为黑色和任何颜色混合,黑色不贡献颜色值)。
混合完了再乘以2,把减半的数值翻回来:
- 如果开关信号是白色,混合结果是主题色减半,再翻倍,正好回到白色
- 如果开关信号是黑色,混合结果是主题色,翻倍后变成了两倍主题色,但RGB通道最大值是255,超过255的会被截断,所以实际上就是主题色本身
这套玩法在Chrome和Firefox里跑得顺溜,Safari 18以下版本有点水土不服,但可以做个回退方案,用纯黑白的版本兜底。
.element {
--bg-color: #ff6b6b;
--base-color: #1e88e5;
--switch: oklch(from var(--bg-color) round(1.21 - L) 0 0);
color: oklch(from var(--bg-color) round(1.21 - L) 0 0); /* Safari兜底 */
}
@supports (color: rgb(from color-mix(in srgb, red, red) r g b)) {
.element {
color: rgb(from color-mix(in srgb, var(--switch), var(--base-color)) calc(2 * r) calc(2 * g) calc(2 * b));
}
}这样一搞,老浏览器也能正常显示黑白色,新浏览器直接上主题色,体验拉满。
