☰
CSS颜色演进实战:从HEX到OKLCH与color-mix的现代用法
2026/10/16 6:59:31 网站建设 项目流程

写前端的日子久了,回头一看,变化最大的往往不是布局框架,也不是打包工具,反而是最不起眼的CSS 颜色。早期写页面就是red、#fff、rgba(0,0,0,.5)三件套,后来开始用hsl(),再到现在项目里到处是oklch()、color-mix()、相对颜色语法。我从最开始的不理解,到慢慢适应,再到主动用它重构设计系统,整个过程踩了不少坑。这篇东西没有太高深的理论,就是一个前端从业者用 CSS 颜色这么多年攒下来的实践经验:各种颜色格式怎么选、现代颜色语法到底能解决什么问题、主题化和无障碍里有哪些细节,以及我会在现场开发里避开的坑。不管你是刚接触 CSS 的新手,还是已经在写设计系统的老手,应该都能从里面找到点能直接用的东西。

1. 从"red"到color():CSS颜色语法的一路演进

1.1 CSS Color规范走到的位置

先说一个容易被忽略的事实:CSS 颜色并不是一夜之间变成今天这样的。CSS 1 时代只有 16 个颜色关键字,aqua、black、blue、fuchsia这些,写法简单但选择有限。CSS 2 增加了orange,补充了系统颜色关键字。到了 CSS 3,rgb()、rgba()、hsl()、hsla()正式进入标准,同时 6 位和 3 位十六进制写法已经非常成熟。这个阶段奠定了写页面时最常用的颜色表达方式。

真正让颜色体系发生质变的是 CSS Color Module 4 和后续的 Color 5 规范。hwb()、lab()、lch()、oklab()、oklch()、color()这些新函数被加进来,还引入了相对颜色语法、color-mix()混合函数、light-dark()、color-contrast(),连十六进制也补上了 8 位带透明度的写法。我把几个主要阶段的典型能力整理成了表格:

阶段代表能力典型写法解决了什么问题
CSS 1/2颜色关键字red写死有限颜色
CSS 3数值颜色#ff0000rgb(255,0,0)hsl(0,100%,50%)精确控制颜色值
CSS 3半透明通道rgba(255,0,0,.5)透明度叠加
Color 4感知均匀空间oklch(0.7 0.15 25)人眼感知一致的亮度
Color 4/5颜色运算color-mix(in oklch, red 60%, blue)从已有颜色派生新颜色
Color 4广色域输出color(display-p3 1 0 0)适配高色域屏

记住这个演进方向:从"只能写死"到"可以精确表达",再到"可以计算、可以按人类感知调色"。后面所有现代语法,本质上都是在这条逻辑上延伸。

1.2 为什么老一辈的颜色格式越来越不够用

很多人问过我:#ff0000和rgb(255,0,0)明明都能表示纯红,为什么要折腾出一堆新语法?原因有两个,而且都很硬核。

第一个原因是sRGB 色域撑不住了。#rrggbb这套十六进制写法基于 sRGB 色彩空间,这个标准诞生于二十世纪九十年代,当时面对的是 CRT 显示器。今天的 OLED、MiniLED 屏幕已经能显示远超 sRGB 范围的色彩,如果我一直用#0080ff这种写法,等于主动把颜色限制在一个相对狭窄的箱子里。color(display-p3 ...)这类语法就是用来输出更饱和、更鲜艳颜色的。

第二个原因是人眼感知不均匀。RGB 值做算术加减非常简单,但在视觉上并不均匀。rgb(20,20,20)到rgb(40,40,40)的亮度变化非常明显,而rgb(200,200,200)到rgb(220,220,220)的变化就轻得多。调色时我用直觉说"深一点、浅一点、灰一点",RGB 表达这种直觉很吃力。HSL 好一些,但它在纯度和亮度上的表现依然不够贴近人眼。OKLCH、LCH 这类空间就是冲着"感知均匀"来的,在同一个空间里把亮度通道加相同的数值,视觉上变化幅度基本一致。这一条在渐变、阴影、立体感设计里特别重要。

2. 日常工作里最常用的几种颜色写法,差在哪

2.1 HEX与RGBA:普及率高但各有脾气

十六进制是现在项目里出现频率最高的颜色写法。一个 6 位 hex 值#aabbcc等价于rgb(170,187,204),3 位简写#abc会按位复制变成#aabbcc。这俩规则简单,复制粘贴方便,设计稿里给的基本都是 hex。

但它有两个实际问题。一是可读性差,看到一个#4a7fb5,很难马上判断出这是偏蓝还是偏灰、饱和度大致多少。二是 8 位 hex 的兼容性比大家想象中敏感。#aabbccdd这种带透明度的写法,在 CSS Color 4 规范转正之前,只是部分浏览器的扩展支持,旧环境里直接不生效。如果项目需要兼容老旧内核,我通常会把 8 位 hex 显式转成rgba(170,187,204,.87)再上。

rgb()和rgba()的关系也值得说一句。规范里rgba()现在被定义为rgb()的别名,新项目完全可以直接用空格语法rgb(255 0 0 / 50%)代替老式逗号写法rgba(255, 0, 0, 0.5)。不过旧浏览器对空格语法支持不好,团队规范里最好统一一下到底用哪种。

2.2 HSL:调色时最顺手的老朋友

如果只能选一种"符合人类直觉"的传统颜色格式,我投hsl()一票。它把颜色拆成色相、饱和度、明度三个维度,色相 0 到 360 绕一圈,0 是红、120 是绿、240 是蓝,这个认知模型很容易理解。

HSL 最大的价值在于顺着色相环调色。想生成一个同色系色板,只需要固定h值,调整s和l:hsl(200 70% 50%)、hsl(200 70% 40%)、hsl(200 50% 60%),一组和谐的颜色就出来了。这在图表、标签、多模块卡片场景里非常实用。现代写法也支持空格语法加透明通道,例如hsl(200 70% 50% / 0.6),比旧式hsla()更简洁。

但它不是万能的。HSL 建立在 sRGB 空间里,两个饱和度较高的颜色做渐变,中间段经常发灰。另外它的亮度通道也谈不上严格的人眼感知均匀,只是比 RGB 直观很多。真要追求视觉上"同亮度",得到下一节的新空间里找。

2.3 HWB、LAB、LCH与OKLCH:从色彩实验室来的新同学

hwb()的思路特别直白:在一个纯色基础上加白加黑。hwb(200 20% 10%)的意思是色相 200,往纯色里掺 20% 白、10% 黑。理解成本几乎为零,适合手写一些"不要太刺眼"的柔和色。

lab()和lch()来自 CIE 色彩科学体系,oklch()则是在此基础上做了感知均匀性改良的色彩空间。我第一次看到oklch(0.7 0.15 25)的时候也蒙了一下,其实拆开看很简单:第一个值是亮度(lightness,0 到 1),第二个值是饱和度也叫彩度(chroma),第三个值是色相(hue,单位度和 HSL 的色相类似)。它和 HSL 最大的区别是,亮度通道更贴近人眼感知,同一行颜色放在一起,看起来视觉重量更整齐。

我用 OKLCH 最典型的场景是饼图和条形图的离散色板。以前用hsl()按固定间隔生成一组颜色,总有几个看起来特别亮、特别跳。换成 OKLCH 之后,固定 L 值,只改色相,整组颜色的视觉重量就稳了很多。

2.4 我给普通项目的选型习惯

写了几年之后,我慢慢形成了自己的固定习惯,供大家参考:

  • 品牌色、固定 UI 色:直接用 6 位 hex,简单可靠,粘贴设计稿最快。
  • 需要透明度、需要交互状态:用rgb()或hsl()空格语法,配合var()使用。
  • 设计系统、动态生成色板、追求视觉一致性:优先用oklch()。
  • 面向高色域屏、品牌对颜色要求高:color(display-p3 ...)配合媒体查询做增强。
  • 极老的环境:构建阶段统一把新语法编译成 hex/rgba 兜底。

选型不是越新越好,关键是看项目维护成本和目标浏览器范围。如果是一个长期维护、需要大量视觉迭代的业务,早点迁移到 OKLCH 和颜色混合,后面能省下不少重复调色的时间。

3. 现代CSS颜色真正的杀器:相对颜色与color-mix()

3.1 一个 mix 解决几百行手写色板

color-mix()是我近几年用得最频繁的颜色函数,它可以在指定的色彩空间里把两种颜色按比例混合。基础语法长这样:

color-mix(in srgb, red 60%, blue); color-mix(in oklch, var(--brand) 70%, white);

第一个参数in ...指定在哪个色彩空间里插值,后面跟参与混合的颜色和比例。过去做一套完整的按钮状态色,要么设计师给一串色值,要么靠预处理器里的lighten()、darken()函数。现在原生 CSS 就能处理:

.btn { background: var(--brand); } .btn:hover { background: color-mix(in oklch, var(--brand) 85%, black); } .btn:disabled { background: color-mix(in oklch, var(--brand) 40%, white); } .btn-soft { background: color-mix(in oklch, var(--brand) 20%, white); }

为什么我会选in oklch而不是in srgb?因为 sRGB 空间里混合两种高饱和颜色,中间色很容易发灰、变脏。换成 OKLCH 之后,混合结果的饱和度过渡要干净得多,尤其在生成柔和背景色时,这个差异肉眼可见。

3.2 from 关键字:相对颜色语法

相对颜色语法是这几天最值得花时间理解的东西。核心思路是基于一个已有颜色,提取它的通道值,经过计算后再组装成一个新颜色。比如做主色的半透明版本:

background: rgb(from #6a5acd r g b / 40%);

from后面跟原始颜色,函数内部的r、g、b分别代表这个颜色在 RGB 空间下的红绿蓝通道值,我可以直接拿来做calc()。更实用的是配合 HSL 或 OKLCH 派生状态色:

:root { --brand: #2d5bd8; --brand-hover: hsl(from var(--brand) h s calc(l - 12%)); }

我只需要定义一次主色变量,hover 色通过相对颜色语法自动计算出来。以后设计师说主色往左偏一格,改一处变量,所有状态色跟着变,再也不用全局搜替代换。

要注意的是,from后面的颜色会先转换到当前函数对应的色彩空间,比如hsl(from #2d5bd8 h s ...),内部拿到的h、s就是该颜色在 HSL 空间下的通道值。也可以对亮度做百分比运算,比如calc(l - 12%),这在生成浅色背景、深色文字时非常顺手。

3.3 一个真实的主题小Demo:变量+相对颜色+color-mix

把上面两样东西组合起来,可以搭一个非常轻量的主题色体系,不用设计系统工具也能维护:

:root { --brand: oklch(0.7 0.15 25); --brand-hover: oklch(from var(--brand) calc(l + 0.05) c h); --brand-active: oklch(from var(--brand) calc(l - 0.05) c h); --brand-soft: color-mix(in oklch, var(--brand) 20%, white); --brand-text: color-mix(in oklch, var(--brand) 30%, black); } .btn-primary { background: var(--brand); color: white; } .btn-primary:hover { background: var(--brand-hover); } .btn-primary:active { background: var(--brand-active); } .btn-ghost { background: var(--brand-soft); color: var(--brand-text); }

这个方案后面再加深色模式、品牌换肤,基本不用重写组件样式。核心原则是:组件永远消费语义变量,不直接消费原始颜色值。我把这个叫三层变量结构,原始色值、语义色、组件专属色,每层之间用var()串联,维护成本降到很低。

4. 实战里的颜色管理:主题、对比度与无障碍

4.1 把颜色变成Token:三层结构的经验

颜色管理落到真实项目里,最重要的不是记住某一种格式,而是把颜色组织成一套可以演进的体系。我推荐三层变量结构:

  1. 原始色层:存放具体色值,比如--raw-brand: oklch(0.7 0.15 25)。
  2. 语义色层:表达用途,比如--color-primary、--color-text-body、--color-bg-page。
  3. 组件消费层:组件里直接写background: var(--color-primary),不关心原始颜色到底是什么。

改主题时只需要动原始色层和部分语义色层,组件样式完全不用碰。这里有一个容易踩的坑:CSS 自定义属性最好存完整的颜色值,不要拆成--h: 220; --s: 80%然后到处拼hsl(var(--h) var(--s) 60%)。拆开存的方案在简单场景能用,但一旦涉及相对颜色、color-mix(),拼接语法会变得极其脆弱,而且没法利用现代语法直接从完整色值派生新色。

4.2 深色模式:light-dark() 的实用度

深色模式过去只能靠媒体查询大范围覆盖样式。现在 CSS Color 5 提供light-dark()函数,一行就能切换颜色:

:root { color-scheme: light dark; } .card { background: light-dark(#ffffff, #1e1e1e); color: light-dark(#1a1a1a, #f0f0f0); }

设置color-scheme: light dark之后,light-dark()会按用户系统的深色/浅色偏好自动选对应的第一个或第二个颜色。这套语法的好处是把颜色切换集中到声明本身,不需要写一堆@media (prefers-color-scheme: dark)覆盖块。

不过要注意适用边界。如果深色模式不只是换颜色,还涉及阴影方向、布局密度、圆角变化,那还是老老实实用媒体查询统一处理。我的经验是:纯颜色切换用light-dark(),结构变化用媒体查询,两者可以共存,关键是别在同一处混用两套逻辑,否则排查样式时很容易精神分裂。

4.3 对比度、无障碍与自动取色

无障碍里最常被问到的就是对比度。WCAG 2.2 对普通正文文字要求至少 4.5:1,大号文字 3:1,装饰性元素可以豁免。这里说的对比度不是简单用眼睛估算,背后有一套相对亮度计算公式:

function luminance(r, g, b) { const [rs, gs, bs] = [r, g, b].map((v) => { const c = v / 255; return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }); return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs; } function contrast(hex1, hex2) { const l1 = luminance(...hexToRgb(hex1)); const l2 = luminance(...hexToRgb(hex2)); return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); }

开发阶段我会用这类脚本批量检查对比度不达标的颜色对,省得设计师和开发互相扯皮。

CSS 规范里还有一个color-contrast()函数,目标是从候选颜色里自动选择满足对比度要求的那个,但目前浏览器支持并不成熟,属于前瞻能力。实际项目中我不会把关键文字颜色交给浏览器自动选,万一选出来的颜色和品牌不一致,视觉上会很突兀。自动取色更适合用在辅助性场景,比如动态生成的标签背景。

4.4 图表与渐变中的颜色区分

图表是颜色管理里最容易出问题的地方。用 OKLCH 固定亮度、调整色相生成一组离散色板,可以保证各项视觉重量一致。但光靠色相区分远远不够,色盲用户的感知里,红绿两色可能完全分不开。所以我在设计图表色板时还会刻意拉开亮度或饱和度差异,让同一张图不只是"颜色不同",而是"明暗也有区别"。这个习惯对无障碍很友好,对打印输出也有帮助。

渐变方面,如果两个关键颜色都在 OKLCH 空间里插值,中间色不会发灰。我在做品牌渐变背景时,默认写法已经改成:

.hero { background: linear-gradient( to right in oklch, var(--brand), var(--secondary) ); }

in oklch可以让渐变中间段保持干净的色彩倾向,而不是变成灰蒙蒙的一片。

5. 颜色相关的坑与排查技巧

5.1 半透明的叠加:肉眼结果和预期不一样

半透明颜色是最容易让人迷惑的坑。rgba(0, 0, 0, 0.5)叠在红色背景上,结果并不是"黑色变淡",而是"红色变暗带一层黑灰"。因为 alpha 混合结果取决于下层颜色:

result = 上层颜色 * alpha + 下层颜色 * (1 - alpha)

两个半透明层叠在一起时,这个公式会递归计算,结果往往和"我想先设一个淡一点的背景色"的预期完全不同。我现在处理这种需求时,更倾向于用color-mix(in oklch, black 20%, transparent)或者直接color-mix(in oklch, var(--bg) 80%, black)生成一个确定的不透明色,而不是依赖两层半透明的叠加。后者在移动端渲染里更容易出现颜色断层。

5.2 颜色断带与位深

深色渐变背景上出现一段段肉眼可见的色阶,这个现象叫"色带",本质是显示设备位深有限,在暗部区域能表达的灰度级数太少。解决思路有两个方向:一是减少渐变跨度,让色阶分布得更稀疏;二是加一点非常轻微的噪点纹理,打断人眼对连续色阶的感知。实操中我在大面积暗色渐变上叠一层半透明噪点图,效果立竿见影。设计评审时如果遇到"背景渐变看着不干净",第一反应不应该是改颜色,而是先排查色带。

5.3 广色域显示器的色域裁剪

普通屏幕只能显示 sRGB 范围内的颜色,当我在 P3 色域写了color(display-p3 1 0 0)这样的颜色,普通屏会把超出的部分强行映射回 sRGB,映射结果经常是"变灰、变脏"。所以稳妥方案是先用 sRGB 色值兜底,再用媒体查询增强:

.box { background: #ff0033; } @media (color-gamut: p3) { .box { background: color(display-p3 1 0 0); } }

我习惯在本地开发时用系统截图工具或浏览器开发者工具检查颜色值,看看当前渲染出的 RGB 是否和设计值一致,以此判断是否被色域裁剪了。

5.4 兼容性排查的实操流程

现代颜色的最大障碍是兼容性。比如相对颜色语法,2024 年后主流浏览器才陆续稳定支持,color-mix()稍早一些,color-contrast()至今基本还是概念阶段。我的排查流程固定是这三步:

第一步,查目标浏览器的版本记录,确认语法是否进入稳定支持列表。第二步,用@supports做能力检测,新语法失败的场景主动降级:

:root { --brand: #2d5bd8; } @supports (color: oklch(0 0 0)) { :root { --brand: oklch(0.6 0.2 255); } }

第三步,在开发者工具的样式面板里点击色块,把显示格式从 oklch 切换到 sRGB,确认降级后的最终渲染色没有偏离品牌太多。

还有一个真实踩过的坑:自定义属性嵌套颜色函数时,不要把变量拼在字符串中间。旧写法background: #{$brand}cc是预处理器的逻辑,原生 CSS 里应该用相对颜色或者color-mix()完成同样的透明化操作。我见过不少项目因为想省事,在var()外面硬拼cc这类透明度后缀,结果是整个颜色声明失效。

写到这里,我相信你已经感受到了 CSS 颜色这些年从"写死"到"算出来"的变化。我个人现在的默认做法是:项目里所有核心品牌色都落到 OKLCH 变量上,状态色一律用相对颜色或color-mix()派生,深色模式靠light-dark()收敛,兼容性用@supports兜底。最后再分享一个小技巧,调试颜色时可以在浏览器开发者工具的样式面板里用快捷键切换颜色显示格式,一行代码都不用改,就能快速判断当前色值在 sRGB 和 OKLCH 之间的差异,平时排查对比度和色域问题非常方便。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询