返回博客

主题色对了照样露黑底:color-scheme 缺失让 macOS overscroll 穿帮

某天在 macOS 上测一个支持明暗双主题的后台管理系统,切到亮色主题、把页面往上猛推了一下——回弹瞬间,顶部/底部闪出一道刺眼的纯黑。完全复现:只要 OS 处于暗色模式、页面手动切到亮色,每次橡皮筋回弹都会穿帮。

第一反应是背景变量没生效。打开 DevTools 查了一圈,getComputedStyle(document.documentElement).backgroundColor 返回的是 lab(98% ...) 这种近白色——背景值完全正确。那黑底从哪来的?

现象

  • macOS 系统外观:暗色
  • 页面主题:通过 data-theme="light" 切到亮色
  • 页面背景色:var(--color-bg) 在亮色态确实是浅色,DevTools 验证无误
  • 但 overscroll(上拉/下推超过边界时的橡皮筋回弹区域)露出纯黑
  • 原生滚动条、<input><select> 也呈暗色系
  • headless 测试环境无法复现,必须真机真浏览器肉眼看

根因

CSS color-scheme 属性控制的不是页面自己的 background,而是浏览器 UA 自己负责绘制的那些表面——overscroll 回弹区、原生滚动条轨道、表单控件(<input><select><textarea>)、以及无页面背景时的根 canvas。

color-scheme 未声明或值为 normal 时,浏览器对这些 UA 表面的渲染策略是:跟随操作系统外观

于是出现了这个组合:

  • OS 暗色 → 浏览器把 overscroll 区画成暗色(黑)
  • 页面亮色 background → 页面内容区正常显示浅色

两层叠在一起,回弹瞬间 UA 表面从内容背后"透出来",就是那道黑。

坑:background 对了,UA 表面不受 background 控制

html/bodybackground-color 再正确,也管不到 overscroll 区、原生滚动条、表单控件。这些表面由 color-scheme 决定跟 OS 走还是跟页面走。不声明 color-scheme = 永远跟 OS 走。

OS 暗色 + 页面亮色 → 每次 overscroll 都会露出系统暗色黑底。

正解

在主题 SSOT(同一个声明 --color-bg 等变量的选择器)里,把 color-scheme 和主题同步声明:

:root {
  color-scheme: light;   /* 亮色默认 */
  --color-bg: #fafafa;
  /* 其他 token ... */
}
 
[data-theme="dark"] {
  color-scheme: dark;
  --color-bg: #0f0f0f;
}

注意不要写 color-scheme: light dark——那个写法意思是"让 UA 按 OS 自己选",正是这个 bug 本身。要写具体值,和 data-theme 保持一一对应。

如果主题值可能是 "system",需在切换脚本里先把 system 解析成当前 OS 的具体 lightdark,再写入 document.documentElement.setAttribute('data-theme', resolved)

正解:color-scheme 跟 data-theme 同源同步

color-scheme: light / color-scheme: dark 放进主题选择器,和背景/前景 token 同一处声明。这样 overscroll 区、原生滚动条、表单控件全部跟页面主题走,不再跟 OS 走。放在 token 层一处,所有消费方(含 npm 包)自动受益。

验证

在 DevTools Console 里验证 color-scheme 是否真的跟随主题切换:

document.documentElement.setAttribute('data-theme', 'light');
getComputedStyle(document.documentElement).colorScheme; // 期望 "light"
 
document.documentElement.setAttribute('data-theme', 'dark');
getComputedStyle(document.documentElement).colorScheme; // 期望 "dark"

然后在 macOS 切到暗色系统外观、页面切回亮色主题,手动下拉回弹——overscroll 区应露出页面浅色背景而非黑色。

更彻底:让外壳不产生滚动

如果是 app-shell 类布局(侧边栏 + 内容区),更彻底的做法是让外层根本不滚:html, body { overflow: hidden; height: 100dvh; },只让内容区 overflow-y: auto; overscroll-behavior: contain;。这样 body 不触发橡皮筋,内层回弹只露自己的(已 themed)背景。

两者配合——color-scheme 管"露出的那层用什么配色画",overscroll-behavior: contain 管"回弹不向上链"——效果最稳。注意这个方案只适合 app-shell 类页面,不要对营销首页全局 overflow: hidden

一句话外卖

主题切换时,color-scheme 必须和 background 同步声明——前者管 UA 自绘的表面(overscroll / 滚动条 / 表单控件),后者管页面内容区,缺一不可。