你以为 color-scheme 只是个声明?今天 light-dark() 把暗色模式变成可编程的了

暗色模式已经成了前端工程师的日常操作,但每次真的要落地的时候,工程上的别扭感总是挥之不去。

举两个典型的工程痛点。

痛点一:两套变量。 项目里写 --text-primary: #1a1a1a;,暗色模式里要写 [data-theme="dark"] { --text-primary: #f0f0f0; }——一个颜色要写两遍,变量名还得一模一样。一旦有人漏写或者写错,要么白屏要么文字消失,CLS 直接爆表。

痛点二:属性重复。 color 写一套,background 写一套,遇到 border-color 再写一套,每套都要用 @media (prefers-color-scheme: dark) 包起来。整个样式表散落着 @media 块,改一个 token 要全局搜索。

这两个痛点本质上是同一个:两套值、一个属性,需要两套声明来管理。

light-dark():一行声明解决两套值的问题

CSS Color Level 5 引入了 light-dark() 函数,语法极简:

/* 第一步:激活 color-scheme 机制 */
:root {
  color-scheme: light dark;
}

/* 第二步:在任意属性里用 light-dark() */
body {
  color: light-dark(#1a1a1a, #f0f0f0);
  background: light-dark(#ffffff, #0f0f0f);
}

button {
  border-color: light-dark(#e5e7eb, #2a2a2a);
}

light-dark(lightValue, darkValue) 接收两个值:当 color-schemelight 或未设置时使用第一个值,为 dark 时使用第二个值。不需要媒体查询,不需要选择器嵌套,一行解决。

浏览器兼容方面,light-dark() 自 2024 年 5 月进入 Baseline Widely Available 阶段,全球覆盖率约 88%(Chrome 123+、Safari 17.5+、Firefox 120+),可以放心在生产环境使用。Safari 17.5 就已支持,比现在大多数项目的最低兼容版本还老。

一个必须前置的条件

light-dark() 正常工作有一个前提:color-scheme: light dark 必须在对应元素或其祖先上设置。如果不写这行,浏览器默认走 color-scheme: light,函数永远返回第一个值——不报错,不报警告,暗色模式静默失效。

正确的起点:

:root {
  color-scheme: light dark;  /* 这行必须有 */
}

写完这行之后,prefers-color-scheme 媒体查询的变化会自动驱动 light-dark() 的切换——系统暗色模式开/关,页面颜色自动响应,不需要一行 JS。

三层 Token 架构:用 light-dark() 搭设计系统

light-dark() 真正的工程价值,在于配合 CSS 自定义属性实现三层 Token 架构。

第一层:Primitive Tokens(原始值)

:root {
  --gray-50: oklch(98% 0.008 60);
  --gray-900: oklch(12% 0.006 60);
  --blue-500: oklch(60% 0.16 250);
  --blue-700: oklch(45% 0.18 250);
}

第二层:Semantic Tokens(语义值 + light-dark())

:root {
  --surface: light-dark(var(--gray-50), var(--gray-900));
  --text-primary: light-dark(var(--gray-900), var(--gray-50));
  --accent: light-dark(var(--blue-700), #60a5fa);  /* 暗色降饱和度 */
  --border: light-dark(#e5e7eb, oklch(25% 0.015 260));
}

第三层:Component Tokens(组件值)

.button {
  background: var(--accent);
  color: var(--text-primary);
  border-color: var(--border);
}

三层分离的价值在于:改主题色只需要动第二层(语义层),组件代码完全不动。这是设计系统最推荐的结构,light-dark() 是目前 CSS 平台层面对这个结构最原生的支持。

三个必须知道的坑

坑一:单独设置 color-scheme 会覆盖系统偏好。 color-scheme: lightcolor-scheme: dark 会强制指定,不走 prefers-color-scheme。手动切换主题时需要在 JS 里同时设置 color-scheme

document.documentElement.style.colorScheme = isDark ? dark : light;

坑二:暗色模式的阴影是假的。 亮色模式下用 box-shadow 制造层次感,暗色模式下同样做法阴影在深色背景里根本看不见。正确做法是用边框替换阴影:

.card {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);  /* 亮色:阴影 */
  border: 1px solid var(--border);               /* 暗色:边框 */
}

只写 light-dark(box-shadow值, box-shadow值) 不够,因为纯黑 rgba(0,0,0,0.5) 在深色背景上几乎透明。边框配合 OKLCH 亮度控制才是正确解法。

坑三:渐进增强必须做。 Safari 17.5 以下和旧版浏览器不认 light-dark(),会忽略整条声明。fallback 写法:

:root {
  --text-primary: #1a1a1a;  /* fallback */
  --bg-primary: #ffffff;
}
@supports (color: light-dark(white, black)) {
  :root {
    color-scheme: light dark;
    --text-primary: light-dark(#1a1a1a, #f0f0f0);
    --bg-primary: light-dark(#ffffff, #0f0f0f);
  }
}

@supports 块外的 fallback 声明会保留,只有支持的浏览器才会走 light-dark() 路径。

下一步:三个判断

判断一:你的项目现在用的是什么方案?

如果还在用 [data-theme="dark"] 两套变量块,说明 light-dark() 可以直接替换,一行顶两行。如果用 Tailwind 的 dark: class 或者 JS 手动切换,color-scheme: light dark + light-dark() 组合可以覆盖到 CSS 层,减少 JS 和 CSS 之间的同步负担。

判断二:你的 token 层有三级吗?

没有的话,从 Primitive 开始搭,不用一次到位。先把颜色抽成变量,再用 light-dark() 包装语义层。组件代码用语义变量而不是原始值——这一步不需要设计系统基础,自己项目里就能做。

判断三:有没有图片资源的深浅模式需求?

light-dark() 在 Chrome 150+ 和 Firefox 154+ 支持接受两张图片(gradient 或 url),不仅仅是颜色。同一个属性可以切换图片资源,比如深浅模式用不同的 hero 背景图:

.hero {
  background-image: light-dark(
    url("/images/hero-light.png"),
    url("/images/hero-dark.png")
  );
}

不需要媒体查询,不需要 JS,直接一行搞定。


color-scheme: light dark + light-dark() 这套组合,把暗色模式从「两套 CSS 文件或两套变量」变成了「一个声明式函数」,是 CSS Color Level 5 最实用的新特性之一。搭设计系统从这套开始,往后的 token 架构会清晰很多。

评论区

0 条评论

登录后可评论。