为什么你的深色模式滚动条是错的?
2026年4月12日 · 2905 字.md
根据 MDN 的说法 prefers-color-scheme 能够侦测到用户在操作系统上的主题偏好,也就是浅色和深色模式。
The
prefers-color-schemeCSS media feature is used to detect if a user has requested light or dark color themes. A user indicates their preference through an operating system setting (e.g., light or dark mode) or a user agent setting.
通常,我们可以通过媒体查询在写 CSS 的时候控制全局的主题变量。比如,我们可以写出下面的代码:
:root{
/* light mode goes here */
}
@media (prefers-color-scheme: dark) {
:root{
/* dark mode goes here */
}
}
这样,页面主题可以自适应用户的偏好而自动切换。原生组件也可以自适应。例如浏览器的滚动条,您可能没有注意过
通常,我们可以通过设置 color-scheme 来控制可用的主题,例如:color-scheme - CSS | MDN
:root {
color-scheme: light dark;
}
@media (prefers-color-scheme: light) {
.element {
color: black;
background-color: white;
}
}
@media (prefers-color-scheme: dark) {
.element {
color: white;
background-color: black;
}
}
其中 color-scheme: light dark 告诉了浏览器尊重用户的主题偏好来控制主题的显示。举一反三,如果设置 color-scheme 为 only light 或 only dark 就覆盖了用户的主题偏好,转而显示单一主题。
看起来很不错,但是,在实际应用角度上,原生的 CSS 似乎显得有那么力不足。有的时候,我们需要给出一个切换主题的按钮,这个按钮要覆写掉用户原生的主题偏好。
一般的做法是使用一个 .dark 类,通过添加或移除这个类,可以手动的控制页面的外观主题。例如,在 Shadcn UI 中,有这样一行代码:
@custom-variant dark (&:is(.dark *));
对于 Next.js 框架的项目来说,有一个成熟的解决方案是用 next-themes 这个库next-themes - npm,这个库封装好了 ThemeProvider ,只需要包裹一下主要部分就可以轻松使用。然而,我们在使用 React Router v7 或者类似的框架的时候,由于 UI 库本身没有自带 ThemeProvider 或者我们是手搓的 UI 没有封装好的组件,我们似乎不得不自己或者让 LLM 造一个轮子,用于主题的切换。一个常见的思路是用 JS 向 html 标签添加或移除 dark 类:
function toggleTheme() {
const root = document.documentElement;
const isDark = root.classList.contains("dark");
const nextTheme = isDark ? "light" : "dark";
localStorage.setItem(THEME_KEY, nextTheme);
applyTheme(nextTheme);
}
聪明一点,我们顺带加上 localStorage 的持久化存储:
const THEME_KEY = "theme";
function applyTheme(theme) {
const root = document.documentElement;
if (theme === "dark") {
root.classList.add("dark");
} else {
root.classList.remove("dark");
}
}
function getSystemTheme() {
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
function initTheme() {
const saved = localStorage.getItem(THEME_KEY);
if (saved) {
applyTheme(saved);
} else {
applyTheme(getSystemTheme());
}
}
然后在 HTML 中渲染一个主题切换按钮:
<button onclick="toggleTheme()">切换主题</button>
相信水了这么多,聪明的读者一定会发现这里的一个明显的漏洞:prefers-color-scheme 在某种程度上是和我们手动添加的 .dark 类是冲突的,尤其是在 color-scheme: light dark; 的情况下 ——我们控制了页面的颜色,但没有控制浏览器自己的 UI。
正确的做法是:如果需要手动控制,就不要用 color-scheme: light dark; 而是将 color-scheme 的变换完全交给.dark 类控制。 这是一个很细微的细节,看起来很简单,但是许多网站并没有注意这一点,导致外观上出现了一定程度的脱节。比如 Memos, Memos 是一款很有趣好用开源的自由备忘录软件,前端设计得很现代精致,可惜黑暗模式下侧边栏的滚动条是白色的,这种割裂的设计顿时会让设计的优雅气质衰减。截至我在用的 0.26.0 版本,这个问题仍未解决。猜想应该是因为 Memos 维护了诸多主题的原因,不过有待考证。
例如:
:root {
color-scheme: light;
}
:root.dark {
color-scheme: dark;
}
在某种程度上,prefers-color-scheme 和 color-scheme: light dark; 仍然是最优解,因为我们可以完全依靠浏览器的原生能力而不是繁杂的 JavaScript 脚本来控制这个极其细微的细节。一般来说,用户使用了深色主题,它们在访问网站的时候,往往需要的是一个同样舒服的深色主题,而不是选择手动切换到可能亮瞎眼的浅色模式。某种程度上,如果不是想 Memos 那样维护其它例如 Paper 这样的暖色主题(区分于 light/dark 模式),主题切换按钮往往也是没有必要的、甚至是过度设计的(over-designed)。
所以,下次设计用户界面的时候,不妨先思考一下:用户真的需要手动切换主题吗?或者说,您认为用户反复的手动切换主题对它们来说也算是一种乐趣。如果是后者,那么设计一个按钮、维护一些 JavaScript 也无可厚非,但是如果用户觉得这没必要,那还是删除比较好,毕竟,少即是多嘛。