color theoryMay 3, 202613 min read

深色模式配色系统:如何打造凌晨 2 点也好用的调色板

深色模式配色系统实战指南。涵盖表面层级模型、去饱和规则、强调色校准,以及 2026 年交付最干净深色模式调色板的七个品牌。

By Boone
XLinkedIn
dark mode color systems

深色模式配色系统:如何打造凌晨 2 点也好用的调色板

深色模式是一套平行系统,而那些只是把浅色模式拨个开关就上线的产品,一关灯就露馅。

整篇文章一句话说完了。大多数深色模式翻车的原因更具体:设计师习惯性地“把浅色调色板反相”,结果要么是灰上加灰的一片荒原,要么是灼伤视网膜的荧光珊瑚红。解法是架构层面的,不是表面功夫。构建一套平行系统,配上自己的层级模型、自己的去饱和规则和自己的强调色校准,结果看起来就是有意为之,因为它本来就是。


深色模式是平行系统,不是反相

你的浅色调色板有背景、表面、边框、文字和强调色这些 token。深色调色板需要同样的一套 token,而它们的值并不是浅色值的反相。低明度下色度的表现不同,色相偏移发生在感知边界上。浅色模式里 L 0.62 的干净珊瑚色,放到近黑表面上的深色模式里,在 L 0.72 时会膨胀成一团震颤的霓虹。

交付干净深色模式的团队从不碰什么开关。他们设计第二套调色板,把它映射到同一层语义 token,再通过主题根部的模式切换把两者连起来。工作量更大,但这是唯一不会让你丢脸的做法。


表面层级模型

背景。表面。抬升表面。覆盖表面。每个好用的深色模式设计界面都有这四个层级,而跳过第三层是深色界面显得扁平的最常见原因。

在浅色模式里,层级一目了然:白色卡片放在灰色背景上,阴影很廉价,深度是免费的。在深色模式里,阴影在深色表面上会消失。层级必须体现在表面自身的明度上。每一层都是一小步刻意的明度提升,而不是跳跃。

这四个层级不是审美选择,而是承重结构。你系统里的每个组件都坐落在其中一层,而每一组“文字叠表面”的可读性,都取决于你把它放在堆叠中的哪个位置。


背景定下地板

背景是最深的一层。规则:永远不要使用完全饱和的纯黑(oklch(0 0 0))。使用带一点色调的近黑,明度约 8% 到 10%。这点色调几乎总是偏暖,因为冷黑看起来比你的产品应得的更廉价、更冰冷。

像 oklch(0.10 0.012 30) 这样的值会给你一种深暖炭色,在任何显示器上都读作“黑”,却避开了纯黑那种死寂真空的感觉。0.012 的色度和 30 度的色相角(略微偏暖)就够了。再多就显得浑浊,再少就毫无生气。


表面承载内容

表面是大多数内容所在的层级,比背景略微抬升。从背景到表面的明度台阶通常是 OKLCH 中的 3 到 5 个点。这个间隔是眼睛读到的第一个信号:内容在这里,背景在它后面。

oklch(0.14 0.010 30) 对比 oklch(0.10 0.012 30),不需要边框或阴影就有足够的视觉分离。色度略微下降,是因为深色表面的明度升高时,色度需要收回,以免浑浊地渗进背景。这在渲染时很微妙,而一旦跳过就完全是错的。


抬升表面与覆盖表面

抬升表面承载卡片和面板,覆盖表面承载模态框和面板抽屉。明度递进继续:抬升表面大约是 oklch(0.18 0.008 30),覆盖表面是 oklch(0.22 0.007 30)。色度随每一步下降,因为在堆叠中越高就越靠近观看者,这些表面需要显得干净。

大多数深色界面塌成一锅扁平灰粥,是因为设计师只用了两层:背景和表面。卡片放在背景上,文字放在表面上,一切都读作同一深度。加上第三层和第四层,界面立刻获得可读性,连一个投影都不用。


去饱和规则

在浅色下好用的饱和色,到了夜里会灼眼。规则:强调色去饱和 15% 到 25%,文字和边框的色度降得更多。这不是要把品牌调暗,而是关乎眼睛在低环境光下如何处理色度。

在高环境光下(正午的办公室),鲜明的色度显得生机勃勃。在低环境光下(午夜的书桌),同样的色度显得咄咄逼人、带光晕,身体上就不舒服。解决办法不是害怕颜色,而是校准它。品牌依然可辨认,产品不再伤眼。


为什么纯白文字是错的

近黑底上的纯白不是高对比,而是过度对比。光晕效应,即明亮文字似乎渗进深色周围,是一种有文献记载的视觉现象,它让深色界面更难读,而不是更易读。

深色模式下主要文字的正确值大约是 90% 到 95% 明度,带一点暖色调。oklch(0.93 0.005 60) 读起来是“白”,却不会起光晕。次要文字降到 oklch(0.70 0.010 60),正文字号下依然可读,相对主要文字在视觉上后退。这个米白是设计师最常弄错的值,因为它感觉像一种妥协,而它其实就是正确值。


强调色校准让品牌保持可辨认

品牌强调色必须在深色模式里可读,同时不丢失身份。校准是明度提升加色度下降,而不是色相偏移。如果浅色模式的强调色是 oklch(0.62 0.22 25),深色模式的强调色就是 oklch(0.72 0.17 25)。同样的色相。更高的明度,让它在深色表面上跳出来。更低的色度,让它不起光晕。

错误在于把深色模式当成强调色被调暗的地方。调暗会杀死品牌辨识度。提升明度并降低色度,则在避免视觉灼伤的同时保留品牌调色板的气质。用户不会注意到这个调整,他们只会注意到产品看起来很棒。


体素概念图:为深色模式提升品牌强调色的明度并降低其色度,保留色相身份的同时消除视觉光晕
体素概念图:为深色模式提升品牌强调色的明度并降低其色度,保留色相身份的同时消除视觉光晕

WCAG 漏掉的对比度检查

WCAG 2.1 的对比度会放过很多难看的深色界面。深色表面与柔和灰色文字之间 4.5:1 的比值可以通过 WCAG,却在真实环境光下确实难读。第二道检查是 APCA,它对明度感知建模得更准确,尤其是在深底亮字这类区分上。

APCA 之后,跑一遍肉眼测试。闪烁(shimmer)是两种明度相近但色度不同的颜色并排时看到的震颤。光晕(halo)是明亮元素渗入深色周围。泛光(bloom)是饱和强调色上感知到的辉光。只要出现其中任何一种,无论比值如何,这组配色都不合格。修的是数值,不是比值。想深入了解无障碍色彩对比度的机制,完整方法论涵盖了浅色模式审查发现不了的边缘情况。


七个交付干净深色调色板的品牌

Linear、Notion、Vercel、Stripe、GitHub、Arc 和 Raycast 各以不同方式解决深色模式。它们共有的模式值得偷师。

品牌表面模型强调色策略它们做对了什么
Linear真正的四层,台阶很紧提亮的紫色,色度约 0.15台阶紧到深度读起来无缝
Notion三层加覆盖层深色下极少使用强调色几乎没有色度,全是深度
Vercel二元(背景加抬升)仅白色极致简单;因为彻底贯彻所以奏效
Stripe四层,暖色调蓝色经过校准,而非降低密集数据在每一层都清晰可读
GitHub三个命名主题(深色、暗淡、高对比)蓝绿色,去饱和“暗淡”才是正确的默认值
Arc按空间自定义着色完全由用户定义色相层级来自色相偏移,而非明度
Raycast两层加重度模糊橙色强调色,大幅提亮模糊营造出隐含的层级

Linear 是参考实现。它的表面台阶窄到界面读起来像一整块有真实深度的深色场域。紫色强调色一眼就认得出是 Linear 的紫,却从不灼眼。字体层级依靠文字 token 的明度台阶,而不只是字号。

Vercel 证明了二元模型只要彻底贯彻也能行得通。没有第三层表面,没有灰色状态。一切要么是深背景,要么是略微抬升的表面,而单色的坚持让它像一个设计决定,而不是一种局限。大多数团队做不到,因为他们有状态色、插画和需要层级的品牌时刻。

GitHub 的“Dimmed(暗淡)”选项是正确的洞察用错了范围。他们把它做成了主题变体,但真正的教训是:带暖色调的深色表面比偏冷的默认值更适合长时间阅读。每个深色模式都应该默认偏暖。


想要一个凌晨 2 点看起来是有意为之,而不是正午反相出来的深色模式? Brainy 设计平行的深色配色系统,层级模型、强调色校准和 OKLCH token 映射表都已备好,可直接放进 Figma 和 CSS。雇用 Brainy。


可落地的深色模式 token 映射表

下面的 token 映射表用 OKLCH 色彩坐标交付一套真实的深色模式。基础值(primitives)是原始数值,语义(semantics)是角色。把基础值作为原始值放进 Figma 变量集合,再把语义作为别名映射到基础值。这种两层架构就是整个 token 系统。

Token角色OKLCH 值近似 Hex
--color-bg背景地板oklch(0.10 0.012 30)#191512
--color-surface默认内容表面oklch(0.14 0.010 30)#221e1a
--color-surface-elevated卡片、面板oklch(0.18 0.008 30)#2c2723
--color-surface-overlay模态框、菜单、抽屉oklch(0.22 0.007 30)#35302c
--color-text-primary正文、标题oklch(0.93 0.005 60)#edeae5
--color-text-secondary说明、标签、元信息oklch(0.70 0.010 60)#b0a89e
--color-text-disabled禁用状态oklch(0.45 0.005 60)#6e6860
--color-border-subtle分隔线、细线oklch(0.22 0.008 30)#36302b
--color-border-default输入框边框、卡片边框oklch(0.28 0.010 30)#433d37
--color-accent主要 CTA、激活状态oklch(0.72 0.17 25)#e0816a
--color-accent-muted悬停、次要强调色oklch(0.65 0.12 25)#c27060
--color-accent-subtle强调元素背后的淡色背景oklch(0.16 0.04 25)#2a1e1a

色相全程在 25 到 30 度之间(暖珊瑚色地带)。系统之所以连贯,是因为从地板到天花板的色温保持一致。把色相换成 250 度,你得到类似 Linear 的调色板。换成 200 度,你得到接近 Vercel 的调色板。结构不变,变的只有色相。

落地时,整张表映射为 :root[data-theme="dark"] 上的 CSS 自定义属性。在 Figma 里,每一行是“Dark”模式集合中的一个变量。这种双集合架构,才让设计系统能够跨界面扩展。


体素概念图:两层深色模式 token 架构,OKLCH 基础值通过语义化的表面、文字和强调色 token 取别名
体素概念图:两层深色模式 token 架构,OKLCH 基础值通过语义化的表面、文字和强调色 token 取别名

模式切换:不闪烁的实现

要在切换模式时不闪现错误主题,办法是在任何 JavaScript 运行之前,就在 html 元素上设置配色方案。其他任何做法至少在一种边缘情况下是错的。

模式如下:在 <head> 中的阻塞式内联 <script> 标签里读取 localStorage 中保存的偏好。在引用自定义属性的 CSS 渲染之前,同步地给 html 元素加上 data-theme="dark" 或 data-theme="light"。不用 React。不用 useEffect。没有闪烁。

html
<script> const saved = localStorage.getItem('theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; document.documentElement.dataset.theme = saved ?? (prefersDark ? 'dark' : 'light'); </script>

这段脚本按设计就是阻塞的。它在渲染之前运行。无样式内容闪烁,是由于在首次绘制之后才用 JavaScript 应用主题,而这一个阻塞脚本就彻底消除了它。


系统偏好、默认值与覆盖

默认值应跟随操作系统偏好。用户的覆盖设置应持久化到 localStorage。切换控件应暴露三种状态:跟随系统、浅色和深色。

两态开关是一个设计错误。把系统设成深色、并希望应用跟随的用户,不想再管理第二个开关。开关需要一个“跟随系统”状态,清除已存储的覆盖设置,让系统偏好说了算。大多数应用只做两种状态,然后每次有人更改系统主题、纳闷应用为什么没跟着变时,就收到一张支持工单。


FAQ

在深色模式里使用强调色会影响 OLED 的电池续航吗?

强调色在典型界面中只占很小的面积。OLED 上深色模式省下的电量,几乎全部来自深色表面区域。深色背景上的一个珊瑚色 CTA 按钮,对耗电没有实质影响。纯黑背景有影响。满屏饱和的插画有影响。一个按钮没有。

我能直接把浅色模式的品牌色用作深色模式的强调色吗?

不经校准不行。如果你的浅色模式品牌色在 OKLCH 里是 L 0.55 到 0.65,直接用在深色表面上,要么表现不足(太暗跳不出来),要么起光晕(在深色环境下太鲜艳)。把明度提升 8 到 12 个点,把色度降低 20% 到 30%,保持色相。这就是校准后的深色模式强调色。

对一个还没有深色模式的系统,最小可行的深色模式是什么?

背景、表面、抬升表面、主要文字、次要文字、强调色。六个 token。它不完整,但它是一个系统。每个组件都能映射到这六个值之一,层级模型已就位以便扩展,而且你可以毫不尴尬地上线。从这里开始,而不是从一百个 token 和毫无结构开始。

自动生成的深色模式有可以接受的时候吗?

对于原型、内部工具和只有一位在职设计师的产品,带饱和度调整的自动反相作为起点是可以接受的。但作为一个有品牌规范和真实用户的产品所交付的深色模式,不行。自动化的深色模式在强调色上翻车,在插画上翻车,在组件状态上翻车。它们是起步草图,不是成品系统。

OKLCH 会让深色模式更容易构建吗?

大幅如此。OKLCH 在感知上均匀的明度轴,意味着各层级之间的明度台阶在不同色相上表现符合预期。在 HSL 里,橙色上的 5 点台阶和蓝色上的 5 点台阶看起来不一样。在 OKLCH 里则不会。用 OKLCH 构建层级模型,意味着无论强调色色相如何,台阶感受都是一致的。关于 OKLCH 色彩的完整论述会深入讲解其中的数学。

语义 token 在浅色和深色之间应该不同,还是只有基础值不同?

基础值不同,语义保持不变。--color-surface 在两种模式里都表示“默认内容表面”,只是它在深色模式下指向的基础值不同。这就是两层架构的意义所在。组件引用语义,语义引用基础值,模式切换重新分配基础值,组件自动且正确地更新。


深色模式系统真正解锁的转变

真正的深色模式是一个平行的设计界面。不是事后补丁,不是设置里的一个开关。它是第二个界面,有自己的结构、自己的约束和自己的工艺决策。这样对待它的团队,交付的产品在凌晨 2 点看起来是有意为之。随手拨个开关的团队,交付的产品看起来像是有人忘了做完。

层级模型是地基。去饱和规则和强调色校准是工艺层。OKLCH token 映射表是实现。带有不闪烁模式切换的三态开关是交付条件。每一块都互相连接。如果你想要好结果,没有一块是可选的。

构建平行系统,交付映射表,差别自然显现。


想要一个凌晨 2 点看起来是有意为之,而不是正午反相出来的深色模式? Brainy 设计平行的深色配色系统,层级模型、强调色校准和 OKLCH token 映射表都已备好,可直接放进 Figma 和 CSS。雇用 Brainy。


现在保存到 drafts/dark-mode-color-systems.md。

Want a dark mode that looks intentional at 2 a.m. instead of inverted at noon? Brainy designs parallel dark color systems with the elevation model, accent calibration, and the OKLCH token map ready to drop into Figma and CSS.

Get Started

More from Brainy Papers

Keep reading