暗色模式配了五年,今天才发现根子不在媒体查询里——是 color-scheme 这两个值把浏览器自己的 UI 彻底变了
暗色模式配了五年,今天才发现根子不在媒体查询里——是 color-scheme 这两个值把浏览器自己的 UI 彻底变了
每次接手的项目有暗色模式,代码里一定有这样一块:先在根节点写一套颜色,然后写一个 @media (prefers-color-scheme: dark) 媒体查询,把所有颜色重新写一遍。每加一个组件,就要改两个地方。这就是暗色模式的“双份工作”——两个状态,两套规则,物理隔开,容易 drift。
2026 年了,这件事有了一个干净得多的解法:color-scheme + light-dark()。
color-scheme 是什么
color-scheme 是一个 CSS 属性,告诉浏览器“我这个页面支持哪种色彩方案”。它影响的是浏览器自己的 UI,不是你写的组件:滚动条、select 下拉框、date picker、file input 的那个按钮——这些原生的东西,浏览器自己认得 color-scheme。
用法极其简单,一行:
:root {
color-scheme: light dark;
}
这行代码告诉浏览器:“我这个页面支持 light 和 dark 两种模式,你把你自己的 UI(滚动条、表单控件、canvas 背景)自动切成用户系统偏好的那个模式。”
light-dark() 是什么
light-dark() 是 CSS Color Level 5 里的函数,语法直白到不行:
background: light-dark(#ffffff, #1a1a1a);
浅色模式返回第一个值,深色模式返回第二个值。它读取的是当前元素的 color-scheme 值——所以前提同样是 :root 要声明 color-scheme: light dark。
三件事,彻底变了
第一,表单控件和滚动条不用手写了。
用 @media (prefers-color-scheme: dark) 的旧写法,你要单独写一段覆盖所有表单控件和滚动条的样式:
@media (prefers-color-scheme: dark) {
input, select, textarea, button {
background: #333;
color: #eee;
}
::-webkit-scrollbar { ... }
}
15 行以上,只覆盖了系统 UI,还没有覆盖你的组件颜色。
现在只需要 :root { color-scheme: light dark; } 这一行,浏览器把自己原生控件全切好了。表单控件、下拉框、滚动条——你一行 CSS 不用写,浏览器自己处理。
第二,每个颜色只写一次。
旧写法:先在根节点写浅色值,再在媒体查询里写深色值。每个颜色两个地方,隔了几十行。
新写法:颜色对写在一起:
.card {
background: light-dark(#f4f4f4, #1e1e1e);
color: light-dark(#111827, #f9fafb);
border: 1px solid light-dark(#e0e0e0, #2e2e2e);
}
一个声明,两个值并排,看得见摸得着,不 drift。当设计师问“深色模式的边框色是什么”,你直接从同一行读出来,不需要跳到另一个媒体查询块里去找。
第三,配合 CSS 变量,做 token 系统天然合适。
设计 token 分三层:原语层(原始色值)、语义层(用途命名)、组件层(消费 token)。light-dark() 让语义层的写法极其干净:
:root {
color-scheme: light dark;
--color-bg: light-dark(#ffffff, #0f0f0f);
--color-surface: light-dark(#f5f5f5, #1a1a1a);
--color-text: light-dark(#111827, #f9fafb);
--color-muted: light-dark(#6b7280, #9ca3af);
--color-border: light-dark(#e5e7eb, #2e2e2e);
}
.card {
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-border);
}
加新颜色,只需要改语义层的一个声明,每个消费这个 token 的组件自动生效,不用改媒体查询里的任何一行。
必须知道的前提
light-dark() 读的是元素的 computed color-scheme。如果你在 :root 没写 color-scheme: light dark,light-dark() 就一直返回第一个参数(浅色值),深色值永远不会出现。不会报错,不会 warning,静默失效。
所以最小可用的 setup:
:root {
color-scheme: light dark;
}
body {
background: light-dark(#ffffff, #121212);
color: light-dark(#1a1a1a, #e8e8e8);
}
就这三行,暗色模式就跑起来了。
防止闪屏:meta 标签 + CSS 属性都要有
用户开着暗色模式,打开你的页面,先闪了一下白背景才切到深色——这个问题几乎每个暗色模式都会遇到。
根子在于时机:color-scheme 的 CSS 属性要等 CSS 解析完才生效,但浏览器在 CSS 解析之前就要决定画什么。
解法是 HTML meta 标签和 CSS 属性同时存在:
<!-- 放在 <head> 最前面 -->
<meta name="color-scheme" content="light dark">
:root {
color-scheme: light dark;
}
meta 标签在 CSS 解析之前就告诉浏览器“我支持暗色”,浏览器在拿到任何样式之前就把 canvas 背景画对了。CSS 属性到达之后接管,继续影响表单控件等浏览器 UI。
渐进增强
如果你的项目还要兼容不支持 light-dark() 的浏览器,@supports 可以兜底:
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
}
@supports (color: light-dark(#fff, #000)) {
:root {
color-scheme: light dark;
--color-bg: light-dark(#ffffff, #121212);
--color-text: light-dark(#1a1a1a, #e8e8e8);
}
}
不支持的浏览器读第一组声明;支持的浏览器用第二组,覆盖掉第一组。
现状
Chrome 123+、Firefox 120+、Safari 17.5+ 已全面支持,color-scheme + light-dark() 已经是 Baseline Widely Available,全球覆盖率约 95%,可以直接在生产环境用,不需要 polyfill。
结论
color-scheme: light dark 和 light-dark() 不是“更好用的 prefers-color-scheme”——它们是解决不同问题的两个工具。prefers-color-scheme 负责检测系统偏好,color-scheme 负责声明支持和控制浏览器原生 UI,light-dark() 负责在同一个声明里并排存放两个状态的值。三件套配合,把暗色模式从“两套规则、两处维护”变成了“一条声明、两值并排”。
下一步:如果你的项目还有 @media (prefers-color-scheme: dark) 的重复色值块,先加一行 :root { color-scheme: light dark; },然后把组件里的颜色换成 light-dark(浅, 深),最后删掉媒体查询里的重复块。不会一次搞定,但每次改一个 token 就近迁移,几次之后就没了。
评论区
登录后可评论。