主题色对了照样露黑底: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 表面从内容背后"透出来",就是那道黑。
html/body 的 background-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 的具体 light 或 dark,再写入 document.documentElement.setAttribute('data-theme', resolved)。
把 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 / 滚动条 / 表单控件),后者管页面内容区,缺一不可。