一、以前我们是怎么处理颜色的
做亮暗主题切换的时候,你是不是也经历过这种痛苦:手里攥着十几个甚至几十个颜色变量,手算每种颜色在 light 和 dark 下的对应值,再小心翼翼地塞进根伪类里。一旦产品说“主色再暖一点”,你就得把所有相关的 hover、active 状态重新调一遍。Sass 的 lighten/darken 函数虽然帮忙,但毕竟还要编译,而且最终生成的还是静态的十六进制值。能不能让浏览器在运行时自动帮我们算出合适的变体?现在可以了——color-mix() 来了。
这不是什么实验性的提案,主流浏览器已经实装了这个函数。它可以在CSS里实时混合两种颜色,输出一个新的颜色值。结合自定义属性,我们能构建出一套完全动态的色彩方案,主题切换、悬停态、阴影色都能用简单的计算搞定。这篇文章就和你一起,从语法入手,到搭出一个完整可跑的亮暗主题页面。
二、color-mix() 到底怎么用
color-mix() 的语法非常直白:你告诉它把两种颜色按一定比例在指定的色彩空间里混合就行。比如:
color-mix(in srgb, red 60%, blue 40%);
这句的意思是在 sRGB 空间里,把 60% 的红色和 40% 的蓝色搅拌一下,得到一个偏紫的颜色。你也可以用百分比来表示色标的比例,如果两个色标的总和不是100%,浏览器会自动按比例归一化。
色彩空间除了 srgb,常用的还有 oklch、oklab、hsl 等等。用 oklch 混合出来的颜色过渡更均匀,不容易出现灰暗的中间色,做主题系统的时候我一般会优先考虑它。
三、动态主题的基石:定义基础色调
要实现一个能在亮暗模式之间流畅切换的页面,第一步是确定你的“种子颜色”。这里我们选一个蓝紫色作为品牌主色,然后通过 color-mix 派生出文字色、背景色、边框色以及各种交互状态。
:root {
--primary: #5B5FEE;
--bg-light: #ffffff;
--bg-dark: #141629;
--text-light: #1f2328;
--text-dark: #e6edf3;
}
接着,我们不再直接把这些值赋给元素,而是用 color-mix 来生成实际使用的颜色变量。例如,我们要让暗色模式下背景往主色微微靠拢一点,文字保持高对比度:
:root {
--bg: light-dark(var(--bg-light), var(--bg-dark));
--text: light-dark(var(--text-light), var(--text-dark));
--primary-bg: color-mix(in oklch, var(--bg) 90%, var(--primary) 10%);
--primary-hover: color-mix(in oklch, var(--primary) 80%, black 20%);
--card-bg: color-mix(in oklch, var(--bg) 95%, var(--primary) 5%);
--border: color-mix(in oklch, var(--bg) 60%, var(--text) 40%);
}
这里用了 light-dark() 这个函数,它会根据用户的 prefers-color-scheme 自动选择第一个或第二个值。和 color-mix 打配合,我们就有了既能响应系统主题,又能动态计算衍生色彩的一套变量。
四、做一个能切换主题的页面
光说不练可不行,我们来搭一个极简的页面。页面上有一个导航栏、几张卡片,还有一两个按钮。用户可以点击右上角的切换按钮,在亮暗主题之间反复横跳。
<div class="app">
<header class="navbar">
<span class="logo">ColorMix Demo</span>
<button id="theme-toggle">切换主题</button>
</header>
<main class="content">
<div class="card">
<h3>卡片标题</h3>
<p>这是一张卡片,背景色由 color-mix 生成。</p>
<button class="btn">操作按钮</button>
</div>
<!-- 更多卡片 -->
</main>
</div>
CSS 部分,我们把刚才的变量用上:
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: system-ui;
transition: background 0.3s, color 0.3s;
}
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background: var(--card-bg);
border-bottom: 1px solid var(--border);
}
.card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
margin: 1rem;
}
.btn {
background: var(--primary);
border: none;
color: white;
padding: 0.5rem 1.5rem;
border-radius: 8px;
cursor: pointer;
}
.btn:hover {
background: var(--primary-hover);
}
为了响应用户手动切换(不考虑系统偏好),我们用一点 JavaScript 给 <html> 添加一个 data-theme 属性,并在 CSS 里用属性选择器覆写变量。但这篇文章的重头戏还是 CSS,所以 JavaScript 只负责切换属性,颜色的计算依然全权交给 color-mix。
const toggle = document.getElementById('theme-toggle');
toggle.addEventListener('click', () => {
const root = document.documentElement;
const current = root.getAttribute('data-theme');
root.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark');
});
然后在 CSS 里,我们根据 data-theme 重新定义 –bg 和 –text:
html[data-theme="light"] {
--bg: var(--bg-light);
--text: var(--text-light);
}
html[data-theme="dark"] {
--bg: var(--bg-dark);
--text: var(--text-dark);
}
注意,那些通过 color-mix 派生的变量(比如 –card-bg、–primary-hover)不需要重复定义,因为它们引用的 –bg 和 –text 已经变了,浏览器会自动重新计算。这套机制的弹性就在这里体现出来——你只要改动核心的几个变量,整个色彩生态就会实时响应。
五、用相对颜色语法微调细节
color-mix 擅长处理两种颜色之间的过渡,但有时候你只想在某个颜色的基础上略微提亮或加深。这时候用 color-mix 配上白色或黑色也可以,不过 CSS 还有另一个好用的小工具:相对颜色语法。它允许你从一个现有颜色出发,调整它的某个通道值。
比如,我们已经有了主色 –primary,想做一种稍微浅一点的版本用于禁用状态。可以这样写:
--primary-disabled: oklch(from var(--primary) calc(L * 1.3) C H);
这句的含义是:从 –primary 的 oklch 表示里取出亮度 L 乘以 1.3,保持色度 C 和色相 H 不变,生成一个更亮的禁用色。同样,深色悬停态可以降低亮度:
--primary-active: oklch(from var(--primary) calc(L * 0.8) C H);
这样我们就不需要手动设计第二套颜色色板,所有变化都由基础主色动态生成,维护成本几乎为零。
六、和预处理器比一比,值不值得换?
用了多年 Sass 的朋友可能会问:这和 mix 函数有什么区别?最大的区别在于:Sass 的 mix 是在编译时把颜色算成一个固定值,而 color-mix 是让浏览器在运行时计算,因此可以绑定到自定义属性上。当用户在页面上实时切换主题时,所有混合色都会跟着变,而预处理器生成的静态值做不到这一点。
当然,color-mix 目前还不支持像 Sass 那样调整颜色的透明度通道(alpha)以外的通道,如果需要调整色相、饱和度之类,相对颜色语法是更好的选择。两者配合,完全能覆盖以往预处理器的调色函数集。
七、兼容性怎么样,能直接上生产吗?
color-mix() 和相对颜色语法已经在 Chrome 111+、Edge 111+、Safari 16.2+、Firefox 113+ 中获得支持,覆盖了绝大多数现代浏览器。如果你的用户群里还有极少数旧版浏览器,可以准备一套 fallback 变量——在 :root 里定义好一套静态的 light 和 dark 配色,然后通过 @supports 查询决定是否启用 color-mix 的动态方案。
@supports (color: color-mix(in srgb, red, blue)) {
/* 在这里使用 color-mix 方案 */
}
这样一来,支持新特性的浏览器享受到灵活的动态色彩,老浏览器也能有一个基本可用的静态主题,体验并不会出现断崖式下降。
八、实践中的一个小提醒
当你在一个页面中大量使用 color-mix 时,可能会担心性能问题。从我目前的测试来看,即使是在移动端设备上,上百个 color-mix 求值也不会产生可感知的延迟。因为现代浏览器对这类数学计算优化得很好,而且颜色一旦计算完成就会被缓存。不过要避免在动画里频繁更改 –primary 这类基础变量并导致全局重绘——好在切换主题本身就不是高频操作,完全不用担心。
九、收尾
color-mix() 看起来只是一个小函数,但它把色彩的控制权重新交给了运行时,让我们可以用声明式的思路去描述“主色偏一点背景色”、“文字色和背景保持一定对比度”这样的设计意图,而不是陷入到无穷无尽的色值微调里。和自定义属性、light-dark()、相对颜色语法组合起来,你可以在极短的代码里构建出一套完整、可维护、并且对用户的系统偏好尊重到位的色彩方案。
下次开新项目的时候,不妨试试不再用调色盘式的几十个变量开局,而是只定几个种子颜色,剩下的全部交给 color-mix 来生成。那种清爽的感觉,试过就知道。

