1. 同一款品牌色为什么能写出七八种样子
上周帮一位朋友看他做的后台页面,设计稿上那个主色是#4F46E5,他顺手写成rgb(79 70 229),觉得没问题。结果到了暗色模式下,按钮的半透明遮罩叠上去以后颜色发灰,和设计稿差了一截,他又手动调了三次数值才勉强对上。这件事特别典型:CSS 颜色表示方法看着是最基础的东西,真到工程里,写法的选择直接决定了后面好不好维护、好不好调、会不会出岔子。
先把话说清楚——CSS 里表示一个颜色,至少有这么几类写法:命名颜色(rebeccapurple这种)、十六进制(#rrggbb)、rgb()/rgba()、hsl()/hsla()、hwb()、lab()/lch()、oklab()/oklch()、color()函数带色域参数,再加上transparent、currentColor两个特殊关键字,以及 CSS Color 5 里的color-mix()和相对颜色语法。它们最终在屏幕上呈现的可能是同一个像素值,但在可读性、可调性、可计算性、兼容性这四个维度上差别巨大。
这篇文章面向的是已经会写 CSS、但没系统捋过颜色语法的人。我打算把每种写法的原理、适用场景、参数怎么算、哪里会翻车,一条条摊开讲,中间穿插我自己踩过的坑和现在固定下来的写法习惯。读完之后你应该能做到:拿到一个设计稿,能判断该用哪种写法;要搭一套主题色系统,知道变量该怎么分层;遇到颜色对比度不达标、渐变发黑、透明度叠加不对这类问题,能自己定位。
1.1 从屏幕发光到 CSS 语法:颜色通道这条链子
要理解为什么写法这么多,得先知道屏幕是怎么出颜色的。绝大多数普通显示器是三个子像素发光,红、绿、蓝,每个子像素的亮度用一个 0 到 255 的整数控制,这就是所谓 8 位通道。rgb(255 0 0)的意思就是红灯拉满、绿蓝全灭。十六进制只是把这个十进制整数换成了两位十六进制,255就是ff,所以#ff0000和rgb(255 0 0)是同一个东西,只不过一个用 16 进制书写,一个用 10 进制书写。
hsl()换了一套坐标系,但终点还是 RGB。色相 H 是角度(0 到 360,0 是红、120 是绿、240 是蓝),饱和度 S 和亮度 L 是百分比。它的价值在于符合人调色的直觉——"这个绿太艳了,饱和度降一点"、"这个蓝太暗了,亮度提一提",用 HSL 改一个数字就行;如果用 RGB,你调亮度要同时动三个值,还容易把色相带偏。浏览器内部会把 HSL 换算回 RGB 再上屏,换算公式不复杂,但不线性,这一点后面会专门讲。
再往上,lab()/lch()/oklab()/oklch()这一层引入了"感知均匀"的概念。人眼对亮度的感知不是线性的,同样是 50% 的数值,黄色看着比蓝色亮得多。sRGB 和 HSL 都以设备通道为基准,所以它们在"亮度"这件事上和人的直觉是脱节的。Lab 系列色彩空间就是为了让数值差和视觉差对应起来而设计的,这也是为什么现在搭色阶、做渐变,越来越多的人往oklch上转。
color()函数则解决另一个问题:色域。普通屏幕能显示的颜色范围叫 sRGB 色域,广色域屏幕上还有 Display-P3。用color(display-p3 1 0 0)能写出比#ff0000更饱和的红,但在不支持广色域的屏幕上会做映射,呈现出来会变淡。这就是为什么很多团队宁愿老老实实待在 sRGB 里——控制得住。
注意:不管写哪种语法,最终都要落到设备的通道值上。所以"同一款颜色"在不同写法下可能出现肉眼可见的差异,原因往往不是浏览器有 bug,而是色彩空间或者取整方式不同。
1.2 六种主流写法横向对比:先看这张表再决定用哪个
我习惯先给结论再讲细节,下面这张表是我自己选型时会过一遍的判断依据。
| 写法 | 典型示例 | 直观性 | 可计算性 | 兼容性 | 适合场景 |
|---|---|---|---|---|---|
| 命名颜色 | tomato | 高 | 无 | 极好 | 原型、调试、少量装饰性颜色 |
| 十六进制 | #4f46e5、#ff000080 | 中 | 弱(要拆通道) | 极好(8 位需较新浏览器) | 设计稿交付、静态色值、设计令牌 |
rgb()/rgba() | rgb(79 70 229 / 50%) | 中 | 中(可拆通道做运算) | 极好 | 需要单独控制透明度、需要通道变量 |
hsl()/hsla() | hsl(244 76% 59%) | 高 | 强(色相/饱和度/亮度可直接乘除) | 极好 | 主题色系统、色阶生成、hover/active 态 |
oklch() | oklch(0.58 0.20 273) | 中 | 最强(感知均匀) | 较新浏览器 | 色阶、暗色模式、渐变插值 |
color-mix() | color-mix(in oklab, red, blue) | 低 | 强 | 较新浏览器 | 派生颜色、动态主题 |
这张表里"可计算性"这一列是我最看重的。举个具体的:设计稿给了主色,要求 hover 时亮度提高 8%、按下时降低 8%。如果主色存成#4f46e5,你得手动算三个通道;如果存成hsl(244 76% 59%),直接写hsl(244 76% 67%)和hsl(244 76% 51%)就行,色相和饱和度完全不动,改出来是一家人。如果存成oklch(0.58 0.20 273),改 L 值到 0.66 和 0.50,视觉上的明暗变化更均匀。
关于兼容性,我的实操判断是这样:rgb()、hsl()、#rrggbb这三个可以无脑用;8 位十六进制(带 alpha)、hwb()、lab()/lch()在近几年发布的浏览器版本里支持已经比较普遍;oklch()和color-mix()属于相对新的特性,如果项目还要照顾老旧设备,务必配@supports回退。具体的回退写法在第五章会给出完整示例。
2. 老牌写法里的细节与暗坑
2.1 十六进制:从 #f00 到 #ff000080,顺序写反就翻车
十六进制是设计稿交付里出现频率最高的写法,因为设计工具导出的就是它。规则很简单:#后面跟 3、4、6、8 位十六进制数字。
- 3 位是 6 位的缩写,每一位重复一次,
#f0a等于#ff00aa。注意只有每一位都相同才能缩,#4f46e5不能缩成#4fe。 - 4 位是 8 位的缩写,
#f00a等于#ff0000aa。 - 6 位是
RRGGBB。 - 8 位是
RRGGBBAA,alpha 在最后两位。
最后这条是重灾区。很多从其他图形软件转过来的人习惯 ARGB 顺序(alpha 在最前),写成#80ff0000以为是半透明红,实际浏览器会读成#80ff00加00透明度,也就是一个黄绿色。我见过一次线上事故就是这么来的,一个浮层的背景色本该半透明,结果变成了一块不透明怪色,排查了半天才发现顺序写反。
alpha 的取值范围是00到ff,00全透明,ff全不透明。换算关系是alpha值 / 255,比如80是128/255 ≈ 0.502,差不多一半。常用的几个值可以记一下:1a约 10%、33约 20%、4d约 30%、66约 40%、80约 50%、99约 60%、b3约 70%、cc约 80%、e6约 90%。
提示:能用 6 位就别用 8 位,透明度尽量交给独立的
opacity属性或者rgb(... / x)去控制。原因是半透明颜色叠在什么背景上,最终呈现完全取决于背景,调试的时候很难一眼看出问题出在色值还是层级。
另一个细节:十六进制不区分大小写,#FF0000和#ff0000等价。团队协作里建议统一小写,代码审查时少一类噪音。还有一点容易被忽略,十六进制的取整是精确的,#808080就是rgb(128 128 128),而用百分比写rgb(50% 50% 50%)得到的是127.5四舍五入到128,绝大多数情况下看不出差别,但在做像素级视觉回归测试的时候,这种细微差异会导致截图比对失败。
2.2 rgb()/rgba() 的新语法:空格和斜杠改了什么
老语法是rgb(255, 0, 0)和rgba(255, 0, 0, 0.5),逗号分隔,alpha 单独一个参数。新语法是rgb(255 0 0)和rgb(255 0 0 / 0.5),空格分隔通道,斜杠引 alpha,并且rgba()和hsla()变成了rgb()、hsl()的别名,四个函数名都能用。
新语法最重要的变化不是括号里的写法,而是它可以接受变量拼装。看这段:
:root { --brand-r: 79; --brand-g: 70; --brand-b: 229; } .btn { background: rgb(var(--brand-r) var(--brand-g) var(--brand-b)); } .btn-ghost { background: rgb(var(--brand-r) var(--brand-g) var(--brand-b) / 0.12); }老语法rgba(var(--r), var(--g), var(--b), 0.5)在早期浏览器里也不好使,因为rgba()被当成一个整体函数,变量替换进去后浏览器不保证重新解析。新语法把通道值拆开之后,这种"存通道、拼颜色"的模式变得稳定可靠,这也是现在做主题系统的主流做法之一。
通道的取值也有讲究。整数0-255是传统写法,百分比0%-100%是新语法支持的,两者可以混着用(虽然不建议)。alpha 可以是0-1的小数,也可以是百分比,/ 50%和/ 0.5等价。
这里有个隐藏的坑:alpha 为 0 的颜色不是"没有颜色",而是一个透明的颜色。rgb(255 0 0 / 0)和rgb(0 0 255 / 0)在屏幕上看起来都是"什么都没有",但如果拿去做渐变,结果完全不同。这个坑在 4.2 节会详细展开。
2.3 hsl() 最好调色,但它的"亮度"不是你以为的亮度
hsl(H S% L%)三个参数:色相角度(可以带单位deg,也可以省略),饱和度百分比,亮度百分比。我用它的频率最高,因为调色确实顺手。几个固定结论记一下:
L = 0%永远是黑,L = 100%永远是白,跟色相无关。S = 0%永远是灰阶,L决定深浅。- 纯色的饱和度和亮度大致在
S = 100%、L = 50%附近,比如hsl(0 100% 50%)就是纯红,和#ff0000完全一致。 - 色相环上,红 0、黄 60、绿 120、青 180、蓝 240、品红 300。相邻 30 度左右的色相配在一起比较和谐。
问题出在第三点上。很多人以为hsl(60 100% 50%)(纯黄)和hsl(240 100% 50%)(纯蓝)的"亮度"是一样的,因为它们 L 值都是 50%。实际上,把这两个颜色转成灰度图看,黄色会明显比蓝色亮得多。按 WCAG 的相对亮度公式算一下(公式在 5.1 节给出),纯黄的相对亮度约0.928,纯蓝约0.072,差了将近 13 倍。这意味着什么?意味着如果你用 HSL 生成一套"同亮度"的色阶,黄、绿那几个颜色在深色背景上会刺眼,蓝色系则会糊成一团。
我踩过最惨的一次是个图表项目,八个系列色用 HSL 均匀取色相,L 固定 50%。上线后收到反馈说黄色那条线在浅色背景上几乎看不见,蓝色那条线在深色背景上也是。后来改成oklch,把 L 统一压到 0.62 左右、色相均匀分布,问题一次性解决。
还有一个 HSL 的实际麻烦:它不好做"同色系不同深浅"的色阶。如果你把 L 从 20% 一路排到 90%,色相保持不变,会发现在中间某一段颜色会突然"跳"一下,因为 sRGB 到 HSL 的映射在两端是非线性的。这就是为什么设计系统里的色阶,用oklch生成出来的观感更接近。
不过 HSL 也不是不能用。我的经验是:调 hover/active 这种小幅度的亮度变化,HSL 完全够用,改 5% 到 10% 的 L 值即可;要做 10 级色阶、要做暗色模式整体映射,就该换oklch了。
3. 新语法把颜色变成了可计算的量
3.1 hwb、lab、lch:为什么必须引入感知均匀空间
hwb()是"色相 + 白度 + 黑度",写法hwb(200 20% 30%),意思是色相 200 度,混入 20% 白、30% 黑。它的直观之处在于:白度加黑度如果超过 100%,会按比例归一化,白度 100% 就是纯白,黑度 100% 就是纯黑。做"给颜色加一点白"这种操作时比 HSL 更符合直觉,但工程里用得不多,知道有这个东西即可。
lab()和lch()是基于 CIELAB 色彩空间的表示法。lab(L a b)里 L 是亮度(0 到 100,或者百分比),a 是绿到红的轴(负值偏绿、正值偏红),b 是蓝到黄的轴(负值偏蓝、正值偏黄)。lch(L C H)是它的极坐标版本,L 亮度、C 彩度、H 色相角。举个参考值:接近纯红的一个 lab 表示大约是lab(54% 81 70),对应的 lch 大约是lch(54% 107 41)。
这一层最核心的价值是感知均匀。所谓感知均匀,通俗说就是"数值上等距变化,人眼看着也等距变化"。在 sRGB 里,#000到#555到#aaa到#fff,人眼感觉中间那几档的间距是不均匀的,暗部跳得厉害。在 Lab 空间里,L 从 0 到 100 均匀分档,肉眼看上去的明暗变化才是均匀的。
这带来两个直接好处。第一,生成色阶不会"某几档挤在一起"。第二,做透明度或明暗的插值动画时,中间过程不会出现奇怪的偏色。很多人做暗色模式是简单地把颜色反转或者降低亮度,用 sRGB 直接算,结果中间会经过一段"发紫"或者"发绿"的颜色,换成oklab插值就没有这个问题。
lab/lch的兼容性在这几年已经很不错了,但如果项目基线比较老,还是要准备回退。回退方式很简单:先写一个rgb()的近似值,再在@supports里覆盖成lch(),浏览器不认识lch()时会直接丢弃那条声明,自动用前面的值。
.tag { color: rgb(200 60 50); } @supports (color: lch(50% 80 40)) { .tag { color: lch(50% 80 40); } }3.2 oklch:现在搭色阶最省心的坐标
oklch()是我目前的主力。它是 OKLab 的极坐标形式,写法oklch(L C H / alpha),L 取值 0 到 1(或百分比),C 是彩度(大约 0 到 0.4,实际很少超过 0.37),H 是色相角度。和lch()相比,它的数值范围更小、更好记,更重要的是它在蓝紫色区域的色相线性表现更好——lch()在蓝色附近做渐变时会经过一段异常的紫色,oklch()基本没有这个问题。
几个实用的参考值,是我自己试出来的,可以直接抄:
- 纯红大约
oklch(0.628 0.258 29) - 纯绿大约
oklch(0.866 0.295 143) - 纯蓝大约
oklch(0.452 0.313 264) - 中等深度的中性灰大约
oklch(0.55 0 0)
搭色阶的做法:固定色相 H,固定彩度 C 附近做小幅调整,L 从 0.95 排到 0.25,中间等距取 10 档,得到的就是一条观感均匀的色阶。比如主色色相是 264,可以这样生成:
:root { --brand-50: oklch(0.97 0.02 264); --brand-100: oklch(0.93 0.04 264); --brand-200: oklch(0.87 0.07 264); --brand-300: oklch(0.79 0.11 264); --brand-400: oklch(0.70 0.15 264); --brand-500: oklch(0.60 0.19 264); --brand-600: oklch(0.52 0.20 264); --brand-700: oklch(0.44 0.18 264); --brand-800: oklch(0.36 0.15 264); --brand-900: oklch(0.28 0.11 264); }注意彩度 C 在两端要收一收,太浅或太深的颜色如果彩度保持很高,会显得荧光或者发脏。上面这条曲线里,C 在 500-600 附近最高,往两头递减,这是我在几个项目里调出来的一个比较稳的手感。当然具体数值还得对着设计稿微调,但这个"中间高两头低"的形状基本通用。
注意:
oklch()里 L 和 C 都要注意钳制。如果 C 给得太大,超出目标色域的边界,浏览器会做映射,最终呈现可能和你预期不一致。保守做法是让 C 不超过 0.25,除非你明确在广色域上工作。
3.3 color-mix() 与相对颜色:把颜色当变量做运算
color-mix()让我少写了很多重复的色值。基本语法是color-mix(in 色彩空间, 颜色A 百分比, 颜色B 百分比),百分比省略时默认各占一半。
.card { background: color-mix(in oklab, var(--brand-500), white 92%); border-color: color-mix(in oklab, var(--brand-500), white 80%); }这里的in oklab是关键。如果用默认的in srgb,混合过程是在设备通道上做线性平均,红加蓝得到的是一个偏暗的紫,观感发闷;用in oklab混合,中间过程是感知均匀的,出来的是明亮的紫。做浅色派生、做浮层遮罩,我一直用oklab。
还有一个用法是插值空间的选择,在渐变里同样生效:
.hero { background: linear-gradient(in oklch, var(--brand-500), var(--accent-500)); }不加in oklch的话,两个高饱和色之间的渐变中间会经过一段"灰蒙蒙"的区域,加了之后过渡会鲜艳很多。这个差异在视觉上非常明显,尤其在做品牌色的彩虹渐变时。
相对颜色语法(CSS Color 5)更进一步,它允许你从一个颜色里"取"出通道再重组:
.btn:hover { background: hsl(from var(--brand-500) h s calc(l + 6)); } .overlay { background: rgb(from var(--brand-500) r g b / 0.15); } .muted { color: oklch(from var(--brand-500) 0.6 0.02 h); }最后那条特别有用:把一个彩色降成同色相的灰色,保留色相、压低彩度和亮度,得到的灰色和主色调性一致,整个界面看起来会"成一套"。以前这个操作要靠设计工具手动出图,现在一行 CSS 解决。
相对颜色的兼容性目前还算新,我的做法是把它当成"增强"而不是"基础":基础色值用变量写死,在@supports (color: hsl(from red h s l))里再覆盖成相对颜色版本。这样老浏览器拿到的是静态值,新浏览器拿到的是动态计算值。
4. 落到项目里:一套可维护的颜色令牌系统
4.1 三层令牌拆分:基础色、语义色、组件色
颜色写法的选择最终要落到组织方式上。我现在的固定套路是三层变量,这个结构是从几个项目里迭代出来的,比"一个变量一个颜色"要稳得多。
第一层是基础色阶,就是上面oklch生成的那套--brand-50到--brand-900,另外再加中性的--neutral-0到--neutral-900。这一层只描述"是什么颜色",不描述"干什么用",命名用色阶数字,不做语义命名。
第二层是语义色,描述用途:--color-bg、--color-text、--color-border、--color-primary、--color-danger、--color-success。这一层全部引用第一层,比如--color-primary: var(--brand-500)。语义色的数量要控制住,一般不超过十五个,多了就没人记得住。
第三层是组件色,只在该组件的作用域里定义,引用语义色:
.card { --card-bg: var(--color-bg); --card-border: var(--color-border); background: var(--card-bg); border: 1px solid var(--card-border); } .card--highlight { --card-bg: color-mix(in oklab, var(--color-primary), white 94%); --card-border: color-mix(in oklab, var(--color-primary), white 78%); }好处是显而易见的。改主题只需要动第二层的映射关系,组件代码一行不用碰。做 A/B 测试换配色,也是改映射。而且这种结构把"颜色写法"隔离在第一层了——第一层用oklch,第二层以后全是var()引用,新语法兼容性问题只影响一处。
有个细节值得说:引用链不要太长。从组件色到语义色再到基础色,三层已经是上限。曾经见过一个项目做到六七层,改个按钮颜色要跳五个文件,那还不如不用变量。
4.2 暗色模式、透明度与渐变的正确姿势
暗色模式不是把颜色反过来。我的做法是:语义色层用light-dark()或者prefers-color-scheme媒体查询做两套映射,基础色阶保持不变。
:root { color-scheme: light dark; --color-bg: light-dark(var(--neutral-0), var(--neutral-900)); --color-text: light-dark(var(--neutral-800), var(--neutral-100)); --color-border: light-dark(var(--neutral-200), var(--neutral-700)); }light-dark()需要配合color-scheme声明使用,写法比写两遍媒体查询简洁很多。如果项目不支持,就老老实实写:
@media (prefers-color-scheme: dark) { :root { --color-bg: var(--neutral-900); --color-text: var(--neutral-100); } }暗色模式里最容易翻车的是半透明遮罩。浅色模式下用黑色rgb(0 0 0 / 8%)做浮层阴影没问题,暗色模式下继续用黑色就完全看不见了,得换成白色低透明度,或者干脆单独定义一套阴影变量。这个不是技术问题,是设计问题,但代码上要留出这个口子。
透明度这块有一个必须牢记的结论:transparent等价于rgb(0 0 0 / 0),是一个透明的黑。所以从红色渐变到transparent,中间经过的是"发黑的暗红",而不是"变淡的红"。我在早期项目里被这个坑了不止一次,尤其是在做图片遮罩的时候。
/* 错误:中间会发灰发黑 */ .overlay { background: linear-gradient(to bottom, var(--brand-500), transparent); } /* 正确:用同色相的透明版本 */ .overlay { background: linear-gradient( to bottom, var(--brand-500), rgb(from var(--brand-500) r g b / 0) ); }如果不支持相对颜色,就用color-mix或者手写同色相的rgb(... / 0)。这个改动视觉上的差别非常明显,值得单独记一条。
还有一个透明度相关的计算需要理解,就是 alpha 合成公式。上层颜色 A(透明度 α)叠在下层颜色 B 上,最终结果:
结果 = A × α + B × (1 - α)举例子:#ff000080叠在白色上,α = 128/255 ≈ 0.502,那么绿色通道是0 × 0.502 + 255 × 0.498 ≈ 127,蓝色通道同理,红色通道是255 × 0.502 + 255 × 0.498 = 255,最终结果约等于#ff7f7f,一个粉色。知道这个公式有什么用?用处在于排查"为什么这个半透明按钮在每个背景上看起来颜色都不一样"——因为它本来就不一样,这不是 bug。
提示:如果一个半透明元素需要在多种背景上保持视觉一致,就不要用 alpha,改成预计算好的实色。半透明只适合用在"我有意让它跟背景融合"的场景。
5. 排查与踩坑实录
5.1 透明度叠加、对比度不达标、色域回退这三类问题
第一类:透明度叠加。除了上面说的transparent问题,还有一个常见情况是嵌套遮罩。一个半透明容器里再放一个半透明子元素,最终透明度不是简单相加。如果父层0.5、子层0.5,子元素实际的视觉不透明度是1 - (1-0.5)×(1-0.5) = 0.75,不是 0.5 也不是 1。排查的时候,我习惯在开发者工具里逐层检查 computed 出来的背景色,实在看不清就临时把某层的背景设成高饱和的洋红,一眼就能定位是哪一层在起作用。
第二类:对比度不达标。这个是可以算的。WCAG 的相对亮度公式是:先把每个通道值除以 255,得到c,如果c <= 0.03928就取c / 12.92,否则取((c + 0.055) / 1.055) ^ 2.4;然后按0.2126 R + 0.7152 G + 0.0722 B加权求和,得到相对亮度 L。对比度就是(L亮 + 0.05) / (L暗 + 0.05),范围 1 到 21。
拿经典的#777在白色背景上算一下:119/255 = 0.4667,代入幂函数得到约0.1845,三个通道相同,所以 L 就是0.1845。对比度 =(1 + 0.05) / (0.1845 + 0.05) = 1.05 / 0.2345 ≈ 4.48。差一点点到 4.5,也就是正文文字用#777是不达标的,得用#767676才勉强够(约 4.54)。这个结论我见过太多人不知道,一直在用#777或者#999做辅助文字,#999的对比度只有约 2.85,远低于标准。
正文需要 4.5:1,大字号(18pt 以上或 14pt 加粗)需要 3:1。做暗色模式的时候,同一条规则也要过一遍,别以为深色背景上浅字就一定达标。
第三类:色域回退。用color(display-p3 ...)或者高彩度的oklch()时,如果设备不支持广色域,浏览器会做色域映射,结果可能是一个明显发灰的颜色。判断方法很简单,在开发者工具里看这个色值有没有被标记成超出色域,或者截图取色对比。我的处理原则是:品牌色永远定义在 sRGB 范围内,广色域只用来做装饰性的强调,绝不承担信息传递的职责。
5.2 常见问题速查表与几条压箱底的习惯
下面这张表是我自己整理的问题速查,遇到相关现象可以对照着看。
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 渐变中间发黑发灰 | 渐变色里用了transparent | 换成同色相的rgb(... / 0) |
| 8 位 hex 颜色完全不对 | alpha 写在最前面了 | 改成#RRGGBBAA,alpha 在末尾 |
| 色阶里某几档挤在一起 | 用 HSL 或 sRGB 均匀分档 | 换oklch,L 值等距分档 |
| 半透明按钮颜色深浅不一 | alpha 合成受背景影响 | 改预计算实色,或接受这个特性 |
| 暗色模式下阴影消失 | 阴影用了黑色低透明度 | 换白色低透明度或单独定义变量 |
| 高饱和色在不同设备差异大 | 超出 sRGB 色域被映射 | 回到 sRGB,或明确只在广色域使用 |
| 文本对比度测算不通过 | 亮度用了视觉估计 | 按 WCAG 公式实际计算 |
currentColor没生效 | 祖先元素没有设置color | 检查继承链,或在根元素设默认色 |
再分享几条我固定下来的习惯。
第一,颜色值永远进变量,不写裸值。哪怕是#fff也写var(--neutral-0)。写裸值的时候觉得省事,等到要统一换色调的时候就得全局搜索替换,还容易漏。
第二,调试颜色时临时用高饱和色。排查层级、排查透明度的时候,把可疑元素背景临时改成magenta或lime,比盯着半透明灰看效率高十倍。改完记得撤回,别提交上去。
第三,currentColor用得越多,代码越干净。边框、图标、SVG 的fill,只要颜色应该跟着文字走,就写currentColor,这样改文字颜色时图标自动跟着变,不用维护两份。
第四,颜色的命名带上"用途"而不是"外观"。--color-danger比--color-red好,因为失败提示不一定是红的,但一定是"危险"。这个习惯在换主题的时候救过我很多次。
第五,写新语法前先想好回退。oklch配@supports,color-mix配@supports,能回退就回退。别指望所有用户都在用最新版本,一个降级不生效的边框颜色在低版本上就是完全看不见。
最后说一个我自己的体会。颜色这件事,表面上是在选写法,实际上是在选表达意图的方式。用十六进制是在说"就是这个值",用hsl()是在说"我要调它的明暗",用oklch()是在说"我要它在视觉上和其他颜色等距",用color-mix()是在说"它和另一个颜色有关系"。选哪种写法,取决于你想让后来维护这段代码的人读到什么信息。我自己现在的默认是:基础色阶用oklch生成后落成变量,组件里一律引用变量,需要派生的时候用color-mix()或相对颜色。这套组合用下来,一年里因为颜色问题返工的次数,一只手数得过来。