做暗黑模式适配这几年,我踩过不少坑,也看着社区里各种方案起起落落。从最早的 prefers-color-scheme 媒体查询,到后面流行的 CSS 变量主题切换,再到更精细的 color-scheme 属性,大家慢慢把“暗黑模式”这件事做得越来越顺。但在最近重构一个跨平台小工具网站时,我重新翻了一遍 CSS Color Module Level 4 的规范,发现有一个基础能力被严重低估了——CSS 系统颜色(System Colors)。它和暗黑模式的关系,远比大多数人以为的要深。这篇文章我想把这块掰开揉碎讲清楚:CSS 系统颜色到底是什么,它和暗黑模式怎么配合,实践时有什么坑,以及什么时候该用它、什么时候该老老实实写变量。
先说个特别现实的场景。你是不是也遇到过这种情况:页面里有一个 <select> 下拉框,或者一个原生的 checkbox,你明明把页面的背景色切成了深色,结果表单控件还是白底黑字,亮得像黑夜里的探照灯。很多人第一反应是去给控件写一堆自定义样式,改背景、改边框、改内部箭头,改完发现不同浏览器里长得还不一样,越改越崩溃。实际上,CSS 系统颜色就是为这类问题准备的。它直接读取操作系统和浏览器 UI 的主题色,你根本不用自己判断当前是亮色还是暗色,控件自己就能跟着系统走。
这个能力对做暗黑模式的人来说,相当于拿到了一批“活的默认颜色”。它们不是写死的 #fff 或 #000,而是会随着系统主题、浏览器皮肤,甚至用户的系统自定义模式(比如 Windows 的高对比度主题)自动变化的语义化颜色值。理解了这一层,你再看暗黑模式这件事,思路会完全不一样。
1. 先说清楚:CSS 系统颜色到底是什么
1.1 从一段让人困惑的代码说起
最早看到系统颜色,你可能是在某段代码里见过 Canvas、CanvasText 这种开头大写的颜色值。当时我的第一反应是:这是什么变量?怎么和 CSS 变量长得不一样?如果也有这个困惑,很正常,因为系统颜色属于“颜色关键字”,是 CSS 颜色类型体系里的一类特殊值,语法上就和普通颜色一样直接写在 color、background-color 里,但它背后的含义完全不同。
简单理解:普通颜色值(比如 #333、rgba(0,0,0,0.5)、hsl(200, 50%, 50%))是你直接告诉浏览器“给我显示这个具体的颜色”;而系统颜色关键字是告诉浏览器“你给我用系统当前 UI 的元素颜色”。至于这个“系统当前 UI 的元素颜色”具体是什么色值,由操作系统、浏览器皮肤、用户辅助功能设置共同决定,页面开发者管不着,也不需要管。
我整理了一下 CSS Color Module Level 4 里最常用的一批系统颜色关键字,方便对照:
| 系统颜色关键字 | 对应的 UI 元素 | 典型场景 |
|---|---|---|
Canvas |
应用内容或文档的背景色 | 页面主体背景 |
CanvasText |
应用内容或文档的文字颜色 | 页面正文、标题 |
LinkText |
链接文字颜色 | 未访问链接 |
VisitedText |
已访问链接文字颜色 | 点击过的链接 |
ActiveText |
激活状态的链接文字颜色 | 正在点击的链接 |
ButtonFace |
按钮的面板背景色 | 按钮背景 |
ButtonText |
按钮上的文字颜色 | 按钮文字 |
ButtonBorder |
按钮的边框颜色 | 按钮边框 |
Field |
表单输入控件的背景色 | 输入框、下拉框背景 |
FieldText |
表单输入控件的文字颜色 | 输入框、下拉框文字 |
Highlight |
选中文本的背景色 | ::selection 背景 |
HighlightText |
选中文本的文字颜色 | ::selection 文字 |
Mark |
标记文本的背景色 | <mark> 元素背景 |
MarkText |
标记文本的文字颜色 | <mark> 元素文字 |
GrayText |
禁用状态的颜色 | 禁用按钮、禁用输入框 |
AccentColor |
系统强调色 | 表单控件高亮、选中态 |
AccentColorText |
强调色对应的前景色 | 强调色背景上的文字 |
看到这张表,你应该能感受到一件事:这些颜色不是孤立的一两个值,而是一整套覆盖常见 UI 元素的“系统色板”。它们的设计目标,就是让网页元素在默认状态下和操作系统原生 UI 保持视觉上的一致。
1.2 它和 CSS 变量、预处理器变量的本质区别
很多人容易把系统颜色和“用 CSS 变量存颜色”混为一谈。两者确实有相似的使用场景,但本质完全不同。
CSS 变量的工作方式是:你在 :root 里定义 --bg: #fff,然后需要根据暗黑模式去覆盖它。颜色值是你自己定的,切换逻辑也是你写的。而系统颜色关键字是浏览器替你从系统层拿值,你只描述“我要什么语义的颜色”,不关心具体值。
这里有一个尤其容易踩的认知误区:系统颜色不是给你定义在 :root 里的变量,它本身就是一个颜色值,可以直接使用,也可以授权给 CSS 变量。 比如这样写完全合法:
css复制:root {
--page-bg: Canvas;
--page-text: CanvasText;
}
这样既保留了系统颜色的动态性,又可以在项目里以语义化变量名统一管理。
另一个区别在于“切换时机”。CSS 变量的暗黑模式切换,通常要靠 prefers-color-scheme 媒体查询或者 JS 脚本,在某个时机触发覆盖;系统颜色则天然是响应式的,系统主题一变,所有引用它的元素立刻跟着变,不需要任何额外代码。这个特性在后面的实操环节会体现出真正的价值。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统颜色和暗黑模式:它们不是替代关系,而是配合关系
2.1 prefers-color-scheme 的局限性在哪里
说系统颜色之前,得先聊聊现在主流的暗黑模式方案 prefers-color-scheme 媒体查询。这个媒体查询的作用,是让页面感知用户的系统主题偏好,然后开发者可以针对性地覆盖样式。
典型的写法是:
css复制:root {
--bg: #ffffff;
--text: #222222;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
--text: #f0f0f0;
}
}
body {
background-color: var(--bg);
color: var(--text);
}
这套方案的优点是可控性强,适合需要品牌定制的场景。但它有一个绕不开的问题:必须由你来定义所有颜色。一旦你漏掉某个颜色,或者某个第三方组件的内部样式没走你的颜色变量,暗黑模式下就会出现一团糟的视觉效果。尤其像滚动条、表单控件默认填充色、::selection 的背景色这些容易被忽略的地方,经常成为重灾区。
2.2 系统颜色怎么补齐这个盲区
系统颜色解决的核心问题,是“无脑正确的默认值”。它让浏览器基于系统当前主题,自动给出合适的颜色。如果你使用了 Canvas 作为页面背景、CanvasText 作为文字颜色,那么在系统切换暗黑模式时,这些元素会自动变成深色背景、浅色文字,而不需要你再写一行 @media。
我把这两者的差异整理成了一张对比表,方便你做技术选型:
| 对比维度 | prefers-color-scheme + CSS 变量 | CSS 系统颜色 |
|---|---|---|
| 自定义程度 | 高,颜色完全由你控制 | 低,颜色由系统决定 |
| 适配成本 | 需要为所有颜色维护明暗两套值 | 无需维护,直接适配 |
| 覆盖范围 | 只有你显式处理的元素才生效 | 表单、选中态等原生 UI 自动生效 |
| 品牌一致性 | 强,适合品牌视觉 | 弱,偏向系统原生视觉 |
| 高对比度适配 | 需要额外写代码 | 天然跟随系统高对比度 |
所以在真实项目里,这两者不是二选一,而是配合着用。我的经验是:页面级的品牌色用 CSS 变量和媒体查询来控制,保持视觉识别度;系统级的原生 UI 元素(表单、文本选区、按钮、链接等)用系统颜色关键字,交给浏览器去匹配系统主题。 两条腿走路,既有了品牌的一致性,又不会漏掉原生控件的暗黑适配。
2.3 别漏了 color-scheme 这个关键配置
讲系统颜色,就绕不开 color-scheme 属性。很多人在暗黑模式适配时忽略它,结果遇到一团乱麻的问题。
color-scheme 的作用,是告诉浏览器这个页面支持哪些主题方案,让浏览器提前用正确的“配色方案”来渲染一些默认 UI。比如默认的滚动条、表单控件的默认外观、<input> 的默认背景和字体颜色等等。它接受 light、dark、light dark 这样的值。
这里有一个极其重要的细节:系统颜色关键字能否正确工作和 color-scheme 有很强的关联。如果你在一个纯白背景的页面上设置 color-scheme: dark,浏览器会尝试用暗色渲染表单控件和滚动条,但页面背景可能还是亮的,这时候如果用了 Field 这个系统颜色,背景就会变成暗色,看起来就很怪。反过来,如果页面是暗色风格,但没有设置 color-scheme: dark,表单控件的默认外观可能还是亮色的,导致你用了 Field 系统颜色,跟浏览器默认渲染出现冲突。
正确做法是在根元素上声明页面支持的配色方案:
css复制:root {
color-scheme: light dark;
}
这句话的含义是“我这个页面既能亮色也能暗色,请浏览器自己判断”。设置以后,表单控件的默认外观、滚动条、选中态等原生 UI 就会跟随系统主题自动切换。这一步是所有暗黑模式适配的基础,哪怕你完全不用系统颜色关键字,也建议写上。
3. 实操演示:用系统颜色做一个自动适配暗黑模式的页面
3.1 完整示例:从零构建一个原生风格页面
下面我用一个真实的例子,把系统颜色的用法完整演示一遍。假设我们要做一个跨平台的小工具页面,里面有正文、链接、表单输入、按钮、复选框、选中文本这些常见元素,目标是不写一行 @media (prefers-color-scheme: dark),让整页跟随系统明暗自动适配。
先看完整的 HTML:
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统颜色暗黑模式示例</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="card">
<h1>CSS 系统颜色示例</h1>
<p>这是一段正文内容,背景和文字颜色使用的是 Canvas 和 CanvasText。</p>
<p>
这是一个 <a href="#">链接</a>,以及一个
<a href="#" class="visited-example">已访问链接</a>。
</p>
<form>
<label for="name">用户名</label>
<input type="text" id="name" placeholder="请输入用户名">
<label for="bio">个人简介</label>
<textarea id="bio" rows="3"></textarea>
<label>
<input type="checkbox"> 记住我
</label>
<button type="button">提交</button>
<button type="button" disabled>禁用按钮</button>
</form>
<p><mark>这是标记文本</mark>,选中这段文字试试。</p>
</main>
</body>
</html>
再来看关键的 CSS:
css复制:root {
color-scheme: light dark;
}
body {
background-color: Canvas;
color: CanvasText;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.6;
margin: 0;
padding: 2rem;
}
.card {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
border: 1px solid ButtonBorder;
border-radius: 8px;
background-color: Canvas;
}
a {
color: LinkText;
}
a:visited {
color: VisitedText;
}
a:active {
color: ActiveText;
}
::selection {
background-color: Highlight;
color: HighlightText;
}
input,
textarea {
width: 100%;
box-sizing: border-box;
background-color: Field;
color: FieldText;
border: 1px solid ButtonBorder;
border-radius: 4px;
padding: 0.5rem;
margin-bottom: 1rem;
}
button {
background-color: ButtonFace;
color: ButtonText;
border: 1px solid ButtonBorder;
border-radius: 4px;
padding: 0.5rem 1rem;
cursor: pointer;
}
button:disabled {
color: GrayText;
cursor: not-allowed;
}
mark {
background-color: Mark;
color: MarkText;
padding: 0 0.25rem;
}
这套代码如果你在支持 CSS Color 4 的现代浏览器里打开,然后把系统主题从浅色切到深色,会发现整个页面——从背景、文字、链接、选中文本到表单控件——全部自动切换,而且所有配色的对比度都是系统级保证过的,读起来非常舒服。
3.2 关键配置逐行拆解:为什么这样做
现在逐块解释一下上面的代码,这里每一行背后都有讲究。
首先是 :root { color-scheme: light dark; }。这行我前面反复强调过,它是整个暗黑适配的“总开关”。写上它,浏览器才会正确渲染默认控件的外观。如果漏掉,哪怕你设置了 background-color: Field,表单控件内部的一些浏览器默认样式(比如 placeholder 的颜色、小部件的内部结构)可能仍然按亮色方案渲染,就会在暗色背景下“局部过亮”。
其次是 Canvas 和 CanvasText 的搭配。这里有一个细节要注意:Canvas 不是你以为的纯白色或纯黑色。在 Windows 11 的暗色主题下,Canvas 可能是接近 #202020 的深灰色;在 macOS 的浅色主题下,它可能是接近 #FFFFFF 但带极轻微灰度的颜色。它不是用来给你做品牌设计的,而是用来保证“文字内容在这种背景上一定清晰可读”。所以我的建议是:大面积的正文背景、正文文字这些纯内容区域,直接用系统颜色;品牌的按钮色、主视觉色,另用变量去定制。
然后是表单控件的处理。我给 input、textarea 设置了 background-color: Field 和 color: FieldText。这正是解决文章开头那个“下拉框白底刺眼”问题的关键。Field 和 FieldText 会跟随系统主题变化,暗色模式下自动变成深底浅字,而且系统会保证它们之间的对比度符合可读性要求。
复选框这里我特意没写自定义样式,让它保持原生渲染。配合 color-scheme: light dark,浏览器会基于系统当前主题绘制复选框的颜色。如果你希望复选框中选中的对勾颜色用品牌强调色,可以用 accent-color 属性,它和系统颜色 AccentColor 配合很自然:
css复制input[type="checkbox"] {
accent-color: AccentColor;
}
这是 CSS 里两代“强调色”体系的对话:accent-color 是属性,你可以给它赋任意值;AccentColor 是系统颜色关键字,代表系统当前的强调色。赋给它,等于告诉浏览器“用当前系统的原生强调色来渲染我的控件”。
3.3 场景延伸:选中文本、禁用态、标记文本
这些细节平时很容易被忽略,但对整体观感影响极大。
默认情况下,大多数浏览器的文本选中态是蓝底白字。如果你的页面是深色风格,这个蓝色选中有时候会显得特别跳。通过系统颜色把选中态接回系统主题:
css复制::selection {
background-color: Highlight;
color: HighlightText;
}
在暗黑模式下,系统会自己选一套适合深色背景的高亮色,而不是网页上刺眼的那一抹亮蓝。这个配置的成本极低,但暗黑模式观感提升很直接。
禁用文本的 GrayText 也值得注意。它对应系统 UI 中禁用文字的颜色,在浅色模式下通常是偏灰的,在暗色模式下会变成偏暗的浅灰色,并且系统会保证它和 ButtonFace 或 Canvas 背景的对比度。如果你给禁用按钮写的自定义灰色值在某个主题下对比度不足,换成 GrayText 一劳永逸。
Mark 和 MarkText 对应 <mark> 标记文本的背景和前景。默认情况下,<mark> 是亮黄色背景,暗黑模式下如果没有处理,它依然是亮黄色,会很突兀。用系统颜色后,暗色模式下浏览器会自动调整标记色,让它既“有标记感”又不至于刺眼。
4. 踩过的坑与排查技巧:系统颜色的真实一面
4.1 最常见的坑:颜色不随主题变化
很多人试系统颜色时碰到的第一个问题是:明明设置了 CanvasText,但系统从浅色切到深色后,文字颜色纹丝不动。排查思路通常是这两个方向。
先看你的浏览器是否支持 CSS Color Level 4 的系统颜色关键字。这块在现代浏览器(Chrome、Edge、Firefox、Safari 的近期版本)支持得不错,但在老版本浏览器或者某些内置 WebView 里,Canvas、CanvasText 这些值会被当成非法值丢弃,元素就落到你写在后面的 fallback 颜色上。
再检查你是不是在正确的元素上设置了 color-scheme。如果你所在页面没有声明 color-scheme: light dark,有些浏览器对系统颜色关键字的解析可能不会随系统主题更新。我的经验是:全局先在 :root 上声明,再局部用关键字,出问题的概率会小很多。
4.2 高对比度模式与系统颜色的关系
这部分容易被忽略但特别重要:系统颜色关键字在 Windows 高对比度模式下,会跟随系统的“主题配色”被重新映射。这意味着,如果你整个页面都是靠系统颜色来绘制的,那么用户开启高对比度模式后,你的页面会自动适配高对比度配色——这是任何自定义颜色方案都做不到的天然优势。
但反过来说,如果在高对比度模式下,你用了系统颜色却配合了自定义的 border-radius、box-shadow 这些样式,系统高对比度会把很多边框、阴影效果强制剥离,页面可能看起来比预期“平”很多。这个不算 bug,是高对比度模式的特性。我一般建议:在做系统颜色适配时,不要过度依赖阴影和细边框来表达视觉层级,尽量让内容本身的信息结构清晰。
4.3 别把系统颜色当“主题变量”过度使用
我也见过一种相反的误用:博主把系统颜色当成万能主题变量,全站所有颜色干脆全部用 Canvas、CanvasText、ButtonFace,连品牌主视觉色也用 AccentColor。这样做的代价是整站和系统 UI 高度趋同,完全没有品牌识别度,用户打开你的产品会觉得“这网站是不是没做完”。
系统颜色的定位是“默认值”,不是“唯一值”。品牌色、主视觉、关键 CTA 的颜色,还是应该由你自己定义。我的取舍标准是:
- 内容区域、文本阅读区:优先
Canvas、CanvasText,省心且可读性有保障 - 表单、原生控件、文本选区:必须接到系统颜色上,否则暗黑模式下大概率翻车
- 品牌主色、营销重点、产品特色组件:用 CSS 变量 +
prefers-color-scheme控制,保证品牌一致性
4.4 兼容性处理和 fallback 策略
系统颜色的兼容性虽然已经在现代浏览器里落地,但如果你需要支持 2020 年以前的浏览器,或者对颜色一致性要求极高的项目,还是要准备 fallback。
一个稳妥的策略是:先写自定义颜色的兜底声明,再写系统颜色关键字覆盖。CSS 的层叠机制会保证:如果浏览器不支持系统颜色,就走前面的自定义颜色;如果支持,就用后面的系统颜色值。
css复制body {
background-color: #ffffff; /* 兜底 */
background-color: Canvas; /* 支持时生效 */
color: #222222;
color: CanvasText;
}
这样即使老旧浏览器不支持系统颜色,页面也只是退化成固定的浅色主题,不会出现“没有背景色”的裸奔状态。
5. 系统颜色之外的暗黑模式补全建议
5.1 与 CSS 变量结合的双层体系
最后分享一个我自己实践下来比较成熟的暗黑模式架构。它在系统颜色之上叠了一层语义化 CSS 变量,兼顾“系统原生适配”和“品牌定制能力”。
css复制:root {
color-scheme: light dark;
--color-bg: Canvas;
--color-bg-elevated: Canvas;
--color-text: CanvasText;
--color-text-secondary: GrayText;
--color-border: ButtonBorder;
--color-accent: AccentColor;
--color-accent-text: AccentColorText;
--color-selection-bg: Highlight;
--color-selection-text: HighlightText;
--color-field-bg: Field;
--color-field-text: FieldText;
}
@media (prefers-color-scheme: dark) {
:root {
/* 只在需要品牌定制覆盖的地方写媒体查询 */
--color-bg-elevated: #252525; /* 自定义:暗色模式下的浮层背景 */
}
}
这种架构的好处是:项目里统一使用 var(--color-bg) 这类语义化变量,后续如果想要重新定义某个主题颜色,只需要改这一个变量声明的位置,不需要去翻组件代码。而 color-scheme: light dark 和系统颜色关键字保证了原生控件和文本选区自动适配,不用为每一个 shadow root 里的细节写媒体查询。
5.2 什么时候该放弃系统颜色
也把“什么时候不该用系统颜色”讲清楚,这样你才不会踩进盲目采用的坑里去。
如果你的目标是做高度定制的品牌暗黑模式——比如你们的深色模式里有特定的深蓝色背景、特定的绿色文字、特定的橙色按钮——那系统颜色不适合你。这种场景下,prefers-color-scheme 配合全套 CSS 变量依然是稳妥的答案。
如果你们的产品需要同时支持 Web 和桌面客户端,并且视觉要求与桌面端原生外观高度一致,那系统颜色 + color-scheme 反而是很大的优势。我曾经在一个浏览器扩展项目里,几乎所有弹窗 UI 都靠系统颜色渲染,省下的维护成本相当可观。
另外一个现实约束是:如果你的访问者大量使用老版浏览器,那系统颜色的兼容性会拖后腿。这时老老实实用变量 + 媒体查询,反而是对用户更负责的做法。
说回我自己的选择。现在做任何新项目,我都会在初始样式阶段就把 color-scheme、Canvas、CanvasText、Field、FieldText、Highlight 这七个最基础的系统颜色接进去,当作“主题默认层”。之后再往上面叠加品牌色变量。这样做最直接的好处是:哪怕我后续完全不写任何暗黑模式的定制样式,页面在暗黑模式下也能保证基本可读性和原生观感。等有需要时再去定制品牌色,效率和最终效果都比从零写两套主题色高得多。
系统颜色这个能力,真正打动我的地方,是它让你意识到暗黑模式的本质并不是“给页面换一层深色皮肤”,而是“尊重系统对颜色的表达能力,让网页成为系统生态的一部分”。这种思路用在当下这个多设备、多主题、多偏好设置的时代,价值只会越来越明显。如果你正在为暗黑模式适配发愁,不妨先放下手头的变量覆盖,试试把系统颜色接进去,也许你会找到一条更省力的路。<||开始重写||
做暗黑模式适配这两年,我把市面上的主流方案基本都试过一遍。最早是给 all 元素加 prefers-color-scheme 媒体查询,后来改用了 root 上的 CSS 变量方案,再往后又开始用 color-scheme 属性去兜底原生控件。每个方案都能解决一部分问题,但总会在某些环节留下别扭感。直到我最近在一个跨平台小工具项目中,真正系统性地使用了 CSS 系统颜色,才意识到这东西被严重低估了。它和暗黑模式的关系,远比“一套默认颜色”更深刻。所以这篇想完整聊聊:CSS 系统颜色到底是什么,和暗黑模式怎么配合,实战中怎么用,以及有哪些只有踩过坑才会知道的细节。
先说一个特别真实的痛点。你有没有遇到过这种场景:页面明明已经适配了暗黑模式,背景、标题、正文全都变成深色了,结果表单里的 <select> 下拉框还是白底黑字,刺眼得很;随手选中一段文字,系统默认的蓝色高亮在深色背景上突兀到没法看;滚动条倒是变暗了,但仔细看又和页面整体风格对不上。这些乱七八糟的“漏网之鱼”,恰恰是暗黑模式适配里最常见的失败现场。很多人为此写了一大堆补丁样式,给 select 写内联背景色,给 scrollbar 写伪元素样式,最后在某个浏览器上又崩了。而这一切的根源,就是你没有让页面“接入系统主题体系”,还在用自己写死的颜色值硬扛。
CSS 系统颜色要解决的,就是这件事。它是 CSS 里一类特殊的颜色关键字,不再代表一个固定的 RGB 值,而是代表操作系统或浏览器当前 UI 某个部分的语义颜色。你只需要告诉浏览器“这里用文字颜色”“那里用输入框背景色”,剩下的事全部交给系统,亮度变了、主题变了、高对比度开了,它都会自动给出合适的色值。
1. 系统颜色是什么:它不是变量,是一套语义色板
1.1 最常用的系统颜色关键字对照
CSS Color Module Level 4 规范里,定义了一长串系统颜色关键字。日常开发里经常用到的,我整理了一张表:
| 关键字 | 对应系统 UI 元素 | 用途示例 |
|---|---|---|
Canvas |
应用内容 / 文档背景 | 页面主体背景 |
CanvasText |
应用内容 / 文档正文颜色 | 正文、标题的文字颜色 |
LinkText |
未访问链接文字 | 全局链接颜色 |
VisitedText |
已访问链接文字 | 访问过的链接 |
ActiveText |
正在激活的链接文字 | 点击瞬间的链接状态 |
ButtonFace |
按钮面板背景 | 按钮背景色 |
ButtonText |
按钮上的文字 | 按钮文字颜色 |
ButtonBorder |
按钮边框 | 按钮、卡片边框 |
Field |
表单输入控件背景 | 输入框、下拉框背景 |
FieldText |
表单输入控件文字 | 输入框内文字颜色 |
Highlight |
被选中文本的背景 | ::selection 背景 |
HighlightText |
被选中文本的前景 | ::selection 文字 |
Mark |
标记文本背景 | <mark> 背景色 |
MarkText |
标记文本前景 | <mark> 文字色 |
GrayText |
禁用态文字 | 禁用按钮、禁用输入框文字 |
AccentColor |
系统强调色 | 单选、复选高亮,accent-color 默认值 |
AccentColorText |
强调色上的前景 | 强调色背景上的图标、文字 |
这些关键字有一个共同特征:它们在不同的系统主题下面,映射到的实际色值会变。亮色模式下,Canvas 可能是接近白色的浅色,CanvasText 是接近黑色的深色,Field 是白色;暗色模式下,Canvas 变成深灰,CanvasText 变成浅灰,Field 变成暗色,整套色板自动翻转。
1.2 和 CSS 变量最本质的区别
很多人会把系统颜色和 CSS 变量放在一起比较,觉得“这不就是默认的变量吗”。我刚开始也是这么想的,后来踩了次坑才意识到区别很大。
CSS 变量是你在 :root 里自己定义的,比如 --bg: #fff。主题切换要靠你写覆盖逻辑,比如用媒体查询把 --bg 改成 #1a1a1a。这个方案的自由度很高,代价是你得维护一套变量,并且要在该用变量的地方都显式声明 var(--bg),漏掉一个就出问题。
系统颜色则完全相反。它的值不是开发者在 CSS 里存的,而是浏览器从操作系统当前主题里现取的。你引用 Canvas,浏览器就去系统问一句“现在文档背景色是什么”,然后直接用。开发者不需要维护任何变量,也不需要考虑切换逻辑,系统一变主题,页面里的 Canvas、CanvasText 全都跟着变。
这里有个很细节的点:系统颜色关键字本身可以作为值赋给 CSS 变量。比如:
css复制:root {
--page-bg: Canvas;
--page-text: CanvasText;
}
这样变量依然存在,但值变成了动态的。项目里可以继续使用 var(--page-bg),但底层的颜色会跟随系统主题变化。这种“变量名 + 动态值”的组合,在我后面的实操环节会派上大用场。
2. 和暗黑模式的关系:默认适配 + 定制覆盖
2.1 prefers-color-scheme 方案少了哪一块
先说清楚一件事:系统颜色和 prefers-color-scheme 媒体查询,不是竞争关系,而是互补关系。
prefers-color-scheme 是让开发者感知系统明暗偏好的“耳朵”,它告诉你用户现在处于哪种主题。用它配合 CSS 变量的典型模式是:
css复制:root {
--bg: #ffffff;
--text: #222222;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
--text: #f0f0f0;
}
}
这套模式能用,但我认为它实际上有一个天然盲区:它只对你显式用变量去覆盖的元素有效。如果页面上有一个你自己漏掉的元素、一个第三方组件内部的样式没走你的变量、或者一个原生控件有自己的默认渲染方式,暗黑模式下它就会“漏出来”,成为你最头疼的那个“亮块”。
如果你全站都是自己写的元素,靠变量体系完全可控,那没问题。但真实项目里,原生表单控件、::selection、滚动条、placeholder 文本、自动填充后的背景……这些元素的样式来源非常分散,要用变量去全部接管,工作量极大,而且每个浏览器对这些默认 UI 的处理细节还不一样。
2.2 系统颜色的“无脑正确默认值”
系统颜色的本质,就是让这些分散的 UI 元素,在默认状态下就能跟随系统主题变化。它不是让你完全放弃定制,而是帮你把所有没定制的部分兜底接进系统主题。用过之后最明显的感受是:你终于不用再盯着控制台一个一个找“漏网之鱼”了。
表格形式总结一下两种方案的适用场景:
| 场景 | prefers-color-scheme + 变量 | CSS 系统颜色 |
|---|---|---|
| 品牌颜色定制 | 适合,完全可控 | 不适合,颜色取决于系统 |
| 大段正文可读性 | 需手动维护两套对比色 | 系统保证基础对比度 |
| 原生表单控件适配 | 需逐控件覆盖 | 默认自动适配 |
| 文本选中态颜色 | 需手动设置 selection | 自动跟随系统选中色 |
| 高对比度辅助模式 | 需额外适配 | 天然跟随系统 |
所以我一般给新手的建议是:大方向的品牌色用变量 + 媒体查询,但那些你有心无力的杂项,通通接回系统颜色。
2.3 别忘了 color-scheme 这个总开关
谈系统颜色必谈 color-scheme。这俩是配合使用的,漏了前者,后者很多时候默默失效。
color-scheme: light dark 声明在根元素上,作用是告诉浏览器:这个页面支持亮色和暗色两种配色方案。浏览器知道这一点后,会提前用正确的配色渲染一批“默认 UI”,比如滚动条、表单控件的默认外壳、地址栏自动填充样式等。如果你不写这个声明,页面上很多元素可能还顽固地按亮色模式渲染,即使你已经给它们设置了 background-color: Field。
我在项目里总是把它放在 CSS 的第一行:
css复制:root {
color-scheme: light dark;
}
这行代码加上之后,原生控件带来的暗黑模式适配痛点能消掉一大半。后来我排查过几次“系统颜色突然不生效”的问题,最后基本都定位到 color-scheme 没设置或者被局部意外覆盖了。
3. 实操演示:一个完全跟随系统主题的页面
3.1 完整代码示例:表单、链接、选中态一次搞定
直接上一个能跑的示例。假设我们要做一个工具类页面,里面包含正文、链接、文本输入、按钮、复选框、禁用态和文本选中态,目标是:不写任何 @media (prefers-color-scheme: dark),页面自动跟随系统明暗切换。
HTML 结构:
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统颜色暗黑模式示例</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="card">
<h1>系统颜色实操示例</h1>
<p>这一段是正文,背景色使用 Canvas,文字使用 CanvasText。</p>
<p>访问一下 <a href="#">这个链接</a>,再看这里 <a href="#">已访问链接</a> 的颜色。</p>
<form>
<label for="username">用户名</label>
<input type="text" id="username" placeholder="请输入用户名">
<label for="bio">简介</label>
<textarea id="bio" rows="3"></textarea>
<label>
<input type="checkbox" name="remember"> 记住我
</label>
<button type="button">提交</button>
<button type="button" disabled>禁用状态</button>
</form>
<p><mark>这是标记文本</mark>,同时你可以用鼠标选中这段文字,观察高亮色。</p>
</main>
</body>
</html>
CSS 部分:
css复制:root {
color-scheme: light dark;
}
body {
background-color: Canvas;
color: CanvasText;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.6;
margin: 0;
padding: 2rem;
}
.card {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
border: 1px solid ButtonBorder;
border-radius: 12px;
background-color: Canvas;
}
a {
color: LinkText;
}
a:visited {
color: VisitedText;
}
input,
textarea {
width: 100%;
box-sizing: border-box;
background-color: Field;
color: FieldText;
border: 1px solid ButtonBorder;
border-radius: 4px;
padding: 0.5rem;
margin-bottom: 1rem;
}
button {
background-color: ButtonFace;
color: ButtonText;
border: 1px solid ButtonBorder;
border-radius: 4px;
padding: 0.5rem 1rem;
cursor: pointer;
}
button:disabled {
color: GrayText;
}
::selection {
background-color: Highlight;
color: HighlightText;
}
mark {
background-color: Mark;
color: MarkText;
padding: 0 0.25rem;
}
你把这份代码在 Chrome 或 Firefox 里跑起来,然后把系统的外观模式从浅色切到深色,会看到背景、文字、链接、输入框、按钮边框、选中高亮、标记文本全部自动翻转,而且整个翻转过程你一行媒体查询都没写。这种体验第一次跑通的时候,说实话还是挺爽的。
3.2 关键步骤解析:为什么这些元素要这么接
这段代码看起来简单,每个颜色的选择背后都有对应的依据。
body 和 .card 的背景用 Canvas 而不是自己心里的那个“深灰色”。原因在于不同系统下 Canvas 的具体色值并不一样,macOS 暗色下偏蓝灰,Windows 暗色下偏暖灰,浏览器还会结合不同外观做微调。直接用系统色值,页面会自然地融入系统观感,而不是显得“这网站自己在硬编码一种灰色”。文字颜色同理,CanvasText 保证了在任何背景下文字都有足够的对比度。
输入框和文本域这里特别关键。我给它们设置 Field 背景和 FieldText 文字。这是修复“暗黑模式下白输入框刺眼”问题的标准动作。很多人在这一步容易犯错:直接把输入框背景设成 transparent,想在暗色下让输入框和页面背景融为一体。结果页面背景用 Canvas 是深灰,输入框透明后确实也是深灰,但边框和文字在亮色主题下又失去层次。我的经验是,输入框这种输入区域,老老实实用 Field 和 FieldText,让系统决定它在亮暗两种主题下的外观,整体协调性最好。
按钮这里有两个细节值得注意。按钮的背景是 ButtonFace,按钮文字是 ButtonText。不同系统下,按钮的观感差异很大:macOS 的按钮偏扁平,Windows 的按钮带一点渐变和立体感。但系统颜色能保证至少颜色层面是协调的。ButtonBorder 在 Windows 亮色主题下是一个比较浅的灰色,暗色主题下会变成一个深一点的轮廓色,用来做可点击区域的轮廓很合适。
::selection 使用 Highlight 和 HighlightText 后,你会发现暗色模式下选中文字的颜色不再是默认的刺眼蓝底白字,而是系统根据主题选好的高亮色。这是一个低成本高感观的优化,强烈建议写进你的全局样式里。
3.3 用 CSS 变量包一层:兼顾美观的推荐姿势
直接裸用系统颜色的缺点是代码可读性确实一般,别人看 CanvasText 一眼也不知道它对应的是页面哪个语义。我推荐在生产项目里包一层语义化变量:
css复制:root {
color-scheme: light dark;
--surface: Canvas;
--surface-elevated: Canvas;
--text-primary: CanvasText;
--text-secondary: GrayText;
--border-strong: ButtonBorder;
--field-bg: Field;
--field-text: FieldText;
--selection-bg: Highlight;
--selection-text: HighlightText;
--brand: AccentColor;
--brand-contrast: AccentColorText;
}
这样页面组件里统一用 var(--surface)、var(--text-primary) 这种变量名,阅读和维护体验都更好。以后如果品牌色升级,想要自定义某个变量的值,只需要在 :root 里覆盖成具体颜色值,其他所有使用位置自动生效,比后期去逐条替换系统颜色关键字要快得多。
4. 实战中的坑与排查技巧
4.1 颜色不跟随系统变化的排查清单
系统颜色在项目中最常见的问题,就是“我明明给它设置了 CanvasText,怎么系统切了暗色,文字还是黑的”。遇到这个问题,我的排查节奏是固定的三步。
第一步,查浏览器支持性。系统颜色关键字需要现代浏览器支持 CSS Color Level 4。老版本浏览器(尤其是 2020 年前的)很可能不识别这些关键字,直接把声明当非法值丢弃。这个可以通过在 DevTools 的 Computed 面板里看颜色有没有生效来确认。
第二步,查 color-scheme 是否设置正确。很多时候问题不是系统颜色本身,而是 :root 上的 color-scheme 配置被覆盖或漏写了。注意,color-scheme 是可以被子元素继承的,如果你在某个组件里写了 color-scheme: light,它就会覆盖全局的 light dark,导致子组件里的系统颜色不再跟随暗色模式。
第三步,查是否有其他更具体的样式覆盖了系统颜色。系统颜色关键字的优先级和普通颜色声明一样,如果别的规则用 !important 或者更靠后的选择器写了具体颜色,系统颜色就被压下去了。这种问题不好排查,建议用 DevTools 看 Computed 样式链路,点开最后一层看是哪条规则把它覆盖掉的。
4.2 高对比度模式下的意外“损失”
这点容易被忽略,但如果你服务的是企业用户、政务系统,高对比度是绕不开的场景。系统颜色在 Windows 高对比度模式下会被系统重新映射,页面上所有使用 CanvasText、FieldText 这类关键字的地方,会自动对齐到高对比度配色方案。从无障碍角度看,这是实际收益。
但副作用是:你在系统颜色旁边搭配的一些自定义装饰性样式会显得格格不入。比如你用 Field 做输入框背景,又在上面加了一层 box-shadow: 0 0 0 1px #36cfc9 的品牌色描边。高对比度模式下,系统可能会强制移除这层 box-shadow,输入框的描边就消失了。这不是 bug,而是高对比度模式在“去掉非必要视觉噪音”。如果你的产品依赖这些装饰性样式传达状态,建议用 border 而不是 box-shadow,因为高对比度模式下 border 仍然保留,只是会换成高对比度配色。
4.3 不要把所有颜色都交给系统
说完了无脑用的好处,也得泼一盆冷水。把整站的品牌色、主 CTA 按钮背景、营销 Banner 全部换成系统颜色,是一个典型的错误用法。
系统的默认颜色是“平均值”,它没有品牌倾向,也无所谓审美。你把主按钮背景改成 AccentColor,用户在 Windows 上的默认强调色是蓝色,在 macOS 上是蓝色,在某个 Linux 发行版上可能是绿色或橙色。如果你的品牌色是橙红色,用户在部分系统上看到的按钮就变成了“系统蓝”,品牌识别度直接归零。
所以,建议的边界很清楚:原生 UI 元素(表单、文本选区、禁用态、滚动条)接入系统颜色;品牌视觉类元素依然走自定义变量 + 媒体查询。 一句话总结就是,系统颜色接管“系统该管的部分”,品牌变量接管“品牌该表达的部分”,两者各司其职,互不干扰。
4.4 老浏览器和无支持场景的 fallback 写法
如果你正在维护的老项目里,用户群体里有相当比例还在用旧内核浏览器,裸用系统颜色会出问题。稳妥的 fallback 写法特别简单:先写一个固定颜色值,再写系统颜色,让新版浏览器用系统色,老浏览器用固定色。
css复制body {
background-color: #ffffff; /* 老浏览器回退 */
background-color: Canvas; /* 新浏览器自动跟随系统 */
color: #222222;
color: CanvasText;
}
这种写法的前提是:你确定老浏览器的用户不会遇到亮暗切换的问题。如果你的老浏览器用户量很大,而且很多人在用系统暗黑模式,那老实说,还是用 CSS 变量 + 媒体查询方案更靠谱,起码每一处颜色都是你有意控制的。
5. 更进一步:系统颜色之外的暗黑模式体系
5.1 color-scheme 切换的完整场景
聊到最后,再补充两个和暗黑模式经常一起出现的零碎场景。第一个是滚动条。如果你设置了 color-scheme: dark,WebKit 内核浏览器会把滚动条渲染成暗色,但页面其他部分是亮色的话,会很突兀。反之,如果你想保持亮色滚动条但页面是暗色,也可以给 html 单独设 color-scheme: light,但是要小心它是否会影响其他系统颜色的解析。
第二个是输入框的自动填充。Chrome 在亮色模式下给自动填充的输入框加的默认浅蓝背景,在暗黑模式下如果和系统颜色打架,会出现颜色条块叠加,很丑。解决思路是给输入框自定义 background-color,并配合 autofill 伪类选择器做覆盖,但那又是另一大块知识点。建议先保证 color-scheme 和 Field 都对,自动填充的问题就减轻一大半。
5.2 推荐架构:变量接收系统颜色,媒体查询留作定制
最后结合我自己正在用的方案,再串一遍整体思考。
我现在做新项目时,暗黑模式的架构基本是这样的:
- 根元素声明
color-scheme: light dark; - 全局默认色全部通过语义化变量指向系统颜色关键字,比如
--text-primary: CanvasText - 品牌相关的颜色变量在亮色模式下是固定的品牌色,比如
--brand: #0066cc - 如果需要针对暗黑模式微调品牌色(比如把品牌色稍微提亮),再写
@media (prefers-color-scheme: dark)覆盖对应变量
这套架构的好处是,它把暗黑模式拆成了两层:第一层是“系统基础适配”,用系统颜色免费获得;第二层是“品牌视觉增强”,用变量和媒体查询做精细控制。两层互不干扰,代码量比单纯媒体查询 + 全量变量少很多,而且覆盖到的地方反而更全。
5.3 什么项目适合直接放弃系统颜色
也把反例讲透。如果你的产品是一个高定制化设计系统,比如你们的设计规范里明确规定了暗黑模式下的所有颜色 token,甚至连文本选区的高亮色都要用品牌蓝,那系统颜色反而会成为包袱。这种情况下,你会特地去覆盖 ::selection 的颜色,也会特意给输入框写死背景色,那系统颜色就没什么必要了。
还有一类项目是强内容型的,比如文档网站、博客、新闻阅读类站点。这种站点对内容的对比度要求极高,对品牌色的要求反而低。用系统颜色 + 语义化变量这套方案,体验会意外地好,因为读者感受到的是“这个网站原生适配了我的设备主题”,而不是“这个网站变了个深色皮肤”。
我在实际项目里最常有的一种体会是:暗黑模式做得好不好,往往不取决于你写了多少 @media,而取决于你省下的颜色覆盖工作有多少。系统颜色把这部分“基础适配”的费用降到几乎为零,让你能把精力放在真正需要品牌表达的环节上。这个思路,在做跨端工具类网站和前端基础组件库时尤其受用。如果你还没有系统性地接入过它,我建议下一个暗黑模式需求里,先试试让 Canvas 和 CanvasText 帮你扛起基础层,你会回来感谢我的。
