深色模式:颜色变量该放哪一层

从零切图

第一版实现:每个组件里写 @media (prefers-color-scheme: dark) { ... }。加了二十个组件之后我改不动了——想微调一个背景色要在二十个文件里各改一次。

问题在于颜色被写到了使用处。正确的分层是三层:

最底层是原始色板,只有色值,没有语义,也不该被组件直接引用:

:root { --gray-900: #16181d; --gray-100: #f2f3f5 }

中间层是语义变量,回答"这个颜色用来干什么":

:root { --bg: #fff; --fg: #1f2328; --muted: #6b7280; --border: #e5e7eb }
[data-theme='dark'] { --bg: #16181d; --fg: #e6e8eb; --muted: #8b929c; --border: #2a2e37 }

最上层是组件,只用语义变量:background: var(--bg); color: var(--fg)。

切换主题时只换中间层的一组值,组件一行不动。

第二件事是首屏闪烁。用 JS 在挂载后设置 data-theme,会先按浅色渲染一帧再跳成深色,肉眼很明显。原因是脚本在 body 末尾。把读取偏好并设置属性的那段内联到 <head> 里、放在样式之前,闪烁就没了。

第三件事是别只反转颜色。深色下纯黑背景配纯白文字对比过高,看久了刺眼;阴影几乎不可见,分隔只能靠边框和亮度差;图片和头像在深色背景上会显得发亮,我给它加了 opacity: .9。这些不是配色技巧,是"深色不是浅色的反相"这件事的具体含义。

最后,跟随系统只是默认值,要给用户一个手动开关并把选择存下来,否则在系统设了深色而用户想要浅色的时候,他只能改系统设置。

相关阅读