深色模式 Token 如何工作
深色模式 Token 是绑定到与浅色主题相同语义名称上的第二套取值。切换模式时,--color-surface 这样的名称会指向另一组颜色,而任何组件都不需要知道当前处于哪个模式。
为什么反色行不通
那个直觉做法——把明度翻过来、黑白对调——做出来的深色主题会以一种起初难以言明的方式显得不对劲。
几个具体原因:
纯黑太生硬。 大多数做得好的深色主题用的是很深的灰,往往还带一点色相,而不是 #000000。纯黑上的满对比白字会产生光晕,字看上去像在洇开。
高饱和的颜色在深色上读起来更亮。 一个在白底上很妥帖的蓝,放在深色背景前可能会发颤。深色模式下的变体通常要降饱和*并且*提亮,而不只是提亮。
层级关系反了过来。 浅色主题里,抬起来的面会投下阴影。深色主题里阴影几乎看不见,所以抬起来的面反而是靠*变亮*来表示的。在一个模式下有效的阴影 Token,在另一个模式下不携带任何信息。
对比度撑不过这个翻转。 一个在浅色下通过 AA 的搭配,在深色下可能不合格。每一种搭配都需要重新检查,而不只是重新计算。
让它成立的结构
深色模式首先是个命名问题,其次才是颜色问题。只有语义层已经存在时,它才做得干净。
:root {
--color-surface: var(--grey-50);
--color-text-primary: var(--grey-900);
}
[data-theme="dark"] {
--color-surface: var(--grey-900);
--color-text-primary: var(--grey-50);
}
组件引用 --color-surface。它们从不知道存在"模式"这回事。以后再加第三套主题,只是再来一块同样形状的代码,而不是去改每一个组件。
如果组件直接引用基础 Token,就没有可以重新指向的东西,深色模式就变成了每个文件里的条件判断。
两套一起生成
半年后再补的深色主题,是把每个组件都过一遍。跟浅色主题一起、从同一套色阶生成出来,则是免费的。
这是即便你只打算先上一套,也应该在一开始就把模式定下来的最有力理由:结构性成本在命名上,而命名在还没有组件的时候很便宜,有了组件之后很贵。
这与 Flash DSM 的关系
每一套生成的体系都同时产出浅色和深色,绑定到同样的语义名上,并且是逐一搭配核对对比度,而不是事后从一个模式推导出另一个。