暗色模式配了五年,今天才发现根子不在媒体查询里——是 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 就近迁移,几次之后就没了。

评论区

0 条评论

登录后可评论。