返回博客

Switch 亮色没问题,深色全黑消失——用了 surface token 的旋钮在暗主题里对比度反转

在给一套组件库做暗主题 QA 时,盯着 Switch 组件看了半天——亮色模式下雪白的旋钮漂亮极了,切到暗模式一看:OFF 状态整个变成一坨黑色,完全分不清旋钮在哪儿。翻代码,旋钮写的是 bg-surface,怎么看都是"正确的" Tailwind CSS 变量用法,为什么会出这种问题?

现象

  • Switch 组件在 亮色模式(light)下:OFF 状态轨道灰、旋钮白,层次清晰。
  • 切换到 暗色模式(dark)后:OFF 状态旋钮和轨道几乎融为一体,甚至和面板背景色一样深,整个控件"消失"。
  • ON 状态(轨道变为主题色 primary)在两个模式下都正常,因为主题色对比度足够。
  • 只看亮色截图做 review 会完全错过这个 bug。

根因

Surface token 是相对于当前主题的颜色变量,它的明暗方向会随主题翻转。当旋钮用 surface token 填色,而轨道恰好也是另一个 surface token 时,两者在暗主题里的对比度方向会同步翻转,旋钮反而比轨道更暗:

| Token(示例) | 亮色模式 | 暗色模式 | |------------------------|-------------|-------------| | --color-surface | white | gray-900 | | --color-surface-hover| gray-100 | gray-800 | | 面板/卡片背景 | white | gray-900 |

一个典型的错误搭配:

  • 轨道(OFF 状态) = bg-surface-hover
  • 旋钮 = bg-surface

亮色下:gray-100 轨道 + 白色旋钮 → 对比清晰。
暗色下:gray-800 轨道 + gray-900 旋钮 → 旋钮比轨道还深,还和面板背景色完全一样 → 控件消失。

坑:surface token 在暗主题里对比方向会反转

旋钮用 bg-surface、轨道用 bg-surface-hover,亮色下白旋钮有对比,暗色下两个 token 同方向变深,旋钮比轨道更暗,和面板背景融为一体。对比度不是"减少了",而是方向翻转——亮色里提供对比的那个方向,在暗色里变成了消除对比的方向。

正解

旋钮(thumb / knob)是一个位置固定的视觉锚点,不需要随主题变色。用固定的浅色(bg-white)替代 surface token:

- <Thumb className="rounded-full bg-surface shadow-md" />
+ <Thumb className="rounded-full bg-white  shadow-md" />

bg-white 的旋钮在两个主题下的表现:

  • 亮色 OFF(灰轨道):白旋钮,对比清晰。
  • 暗色 OFF(深灰轨道):白旋钮,对比清晰。
  • ON(主题色轨道 primary):白旋钮在彩色背景上,对比清晰。

如果设计系统里有 bg-primary-foreground(即"primary 色上的前景色",两个主题下均为白),也可以用它——语义更明确,且和 ON 状态的图标颜色保持一致。

正解:旋钮用固定浅色,不用 surface token

Switch thumb、Slider handle、Radio dot 这类"移动中的视觉指示器",用 固定的 bg-white(或 bg-primary-foreground),而不是主题相对的 surface token。固定浅色在任意主题、任意轨道色上都能维持对比度。

验收方法

四格截图法:对任何有旋钮的控件,必须截到 {light, dark} × {off, on} 全部四个状态,每格都确认旋钮和轨道有明显对比,再 sign off。

light × off   light × on
dark  × off   dark  × on

只看亮色截图、只看 ON 状态——这是这类 bug 能穿越 review 的根本原因。

同族坑:表格斑马行 + 空按钮

同一"surface token 被误用作装饰"的家族里还有两个兄弟坑:

  1. 表格表头/斑马填色:如果表格本身已经放在一个 bg-surface 卡片里,再给表头加 bg-surface-hover,暗色下就变成一个灰色浮块破坏层次——这时该改用透明表头 + 底部分割线,把 fill 去掉。
  2. 空按钮:工具栏的操作按钮必须在对应回调存在时才渲染(features.reload && onReload && <button />),而不是凭 feature flag 渲染再让回调为空——否则消费方不传回调时,按钮点了没反应。

一句话外卖

旋钮 / 指示器用 bg-surface 是天然陷阱:surface token 的对比方向在暗主题里反转,旋钮比轨道更暗从而消失;改用固定的 bg-white,两个主题下永远有对比。