你以为 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-scheme 为 light 或未设置时使用第一个值,为 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: light 或 color-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 架构会清晰很多。
评论区
登录后可评论。