深色模式的适配不只是把背景色改成深灰,如果图标还是深色线条、插图还是白底、输入框仍然是白色,页面会显得支离破碎。资源切换的核心思路是把颜色、背景图、图标这些视觉元素当作变量来管理,而不是散落在各个样式规则里的硬编码值。接下来会从系统检测、CSS变量集中管理、JavaScript切换逻辑和图片资源处理几个方面展开,帮你形成一套可维护的暗色主题方案。

一、借助媒体查询和color-scheme完成基础适配
系统深色模式的偏好可以通过prefers-color-scheme媒体查询来捕获。它有两个常用取值:light和dark,分别表示用户偏好浅色和深色。在CSS里可以直接按这个条件覆盖样式,例如把页面背景和文字颜色改成深色系。
@media (prefers-color-scheme: dark) {
body {
background-color: #15171a;
color: #e6e6e6;
}
}
但仅用媒体查询只能跟随系统,用户想在页面内手动切换深浅主题时就不够用。另一个值得加上的属性是color-scheme,它告诉浏览器当前页面支持浅色和深色,让滚动条、表单控件、默认按钮等原生UI也能自动适配深色外观。将color-scheme: dark放在根元素上,浏览器会尽量把内置控件渲染成暗色,减少手动调整原生控件的工作量。
:root {
color-scheme: light dark;
}
这段声明的含义是页面同时支持浅色和深色,浏览器会依据系统偏好自动选择。如果只写color-scheme: dark,则原生控件始终按暗色处理,适合固定深色主题的页面。
二、用CSS变量统一管理颜色与背景资源
资源切换最容易失控的地方在于图标、插图和背景图散落在不同选择器里。使用CSS自定义属性可以把它们集中定义在:root上,主题变化时只需要替换变量值。下面示例展示了浅色与深色两套变量的定义方式。
:root {
--page-bg: #ffffff;
--text-color: #1f2328;
--border-color: #e1e4e8;
--icon-arrow: url("icon-arrow-light.svg");
--hero-bg: url("hero-light.png");
}
:root[data-theme="dark"] {
--page-bg: #15171a;
--text-color: #e6e6e6;
--border-color: #2d333b;
--icon-arrow: url("icon-arrow-dark.svg");
--hero-bg: url("hero-dark.png");
}
body {
background-color: var(--page-bg);
color: var(--text-color);
border-color: var(--border-color);
}
.icon-arrow {
background-image: var(--icon-arrow);
}
.hero {
background-image: var(--hero-bg);
}
注意:root[data-theme="dark"]选择器表示根元素带有data-theme属性且值为dark时,变量切换为深色版本。这样所有的颜色、边框、图标、插画资源都跟随变量变化,组件内部不需要关心当前主题状态。
使用url()引用图片资源时,变量值本身可以是一个完整的url()表达式。这种写法比在background-image里拼接字符串更直观,也方便在不支持某个资源的平台上做回退。
三、用data-theme属性与JavaScript实现手动切换
系统媒体查询无法满足用户在页面上临时切换的需求,常见的做法是在根元素上动态设置data-theme属性。属性变化会触发CSS变量重新计算,页面资源随之切换。下面的HTML结构展示了按钮和根元素的基本形态。
<html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>深色模式示例</title> </head> <body> <button id="theme-toggle">切换主题</button> <img id="logo" src="logo-light.png" alt="站点logo"> </body> </html>
手动切换还需要读取系统偏好并保存用户选择。下面代码里matchMedia返回一个媒体查询列表对象,通过matches判断当前是否匹配深色模式。localStorage用来记录用户手动选择,刷新页面后仍然生效。
const toggleBtn = document.getElementById('theme-toggle');
const root = document.documentElement;
const mq = window.matchMedia('(prefers-color-scheme: dark)');
function setTheme(theme) {
if (theme === 'dark') {
root.dataset.theme = 'dark';
localStorage.setItem('theme', 'dark');
} else {
delete root.dataset.theme;
localStorage.setItem('theme', 'light');
}
}
function applyTheme(e) {
const saved = localStorage.getItem('theme');
if (saved) {
setTheme(saved);
} else {
setTheme(e.matches ? 'dark' : 'light');
}
}
toggleBtn.addEventListener('click', () => {
const current = root.dataset.theme === 'dark' ? 'dark' : 'light';
setTheme(current === 'dark' ? 'light' : 'dark');
});
applyTheme(mq);
mq.addEventListener('change', applyTheme);
代码里root.dataset.theme对应HTML中的data-theme属性。设为dark时字符串值就是dark,清除属性时可以用delete操作符,这样元素上就不会残留data-theme="undefined"之类的脏值。
如果用户已经手动选择过主题,localStorage中会存在记录,这时系统主题变化也不应该覆盖用户偏好。所以applyTheme函数先检查localStorage,存在记录就直接使用,否则再回退到系统偏好。这是很多主题切换逻辑容易忽略的优先级问题。
四、图片与图标资源的三种切换方式
深色模式下图片资源不一定都要换,但线性图标、插画和透明底PNG往往需要单独准备暗色版本。切换方式主要有三种,适用场景不同。
第一种是通过CSS变量里的background-image切换,前面已经演示过。这种方式适合装饰性背景图和通过CSS绘制的图标,切换时全部由变量驱动,不会出现图片标签加载的布局跳动。
第二种是直接操作img元素的src属性。例如logo图片需要跟随主题改变,可以在setTheme函数中同步更新。
const logo = document.getElementById('logo');
function updateLogo(theme) {
logo.src = theme === 'dark' ? 'logo-dark.png' : 'logo-light.png';
}
这种方式的缺点是切换瞬间浏览器会重新请求新图片,可能出现闪烁或短暂的空白。因此要对关键图片做预加载,后面会提到。
第三种是使用picture元素配合source标签,让浏览器根据媒体查询自动选择资源。下面示例中暗色模式下会加载深色插图,否则加载浅色插图。
<picture> <source srcset="hero-dark.png" media="(prefers-color-scheme: dark)"> <img src="hero-light.png" alt="主题插图"> </picture>
但picture只能跟随系统媒体查询,无法感知页面手动设置的data-theme属性。如果页面提供了手动切换按钮,最好还是用JavaScript统一控制img的src,或者把图片作为CSS背景处理。
五、预加载图片与过渡动画避免闪烁
主题切换时最影响体验的就是图片闪烁。浏览器需要请求新图片,网络稍微慢一点就会出现空白或旧图残留。解决思路是在页面空闲时把另一套主题的关键图片预先加载到内存中。
function preloadThemeImages() {
const images = ['logo-dark.png', 'hero-dark.png', 'icon-arrow-dark.svg'];
images.forEach((src) => {
const img = new Image();
img.src = src;
});
}
window.addEventListener('load', preloadThemeImages);
这段代码在首页资源加载完成后异步预加载深色图片。用户真正切换时,图片大概率已经进入浏览器缓存,显示会平滑很多。还要注意浅色图片通常是默认状态,如果用户可能直接进入深色模式,也可以同时预加载浅色资源,或者只预加载非默认主题的资源。
除了图片,颜色和背景的切换也可以加一点过渡动画,让变化不那么突兀。给body加上transition即可,但不要对background-image做过渡,因为背景图无法平滑渐变,只会触发额外重绘。
body {
transition: background-color 0.2s ease, color 0.2s ease;
}
六、工程化资源命名与维护建议
当项目中的深浅资源变多,命名规范就很重要。建议统一使用-light和-dark后缀,例如icon-arrow-light.svg和icon-arrow-dark.svg。这样在CSS变量中映射清楚,也方便构建工具自动生成不同主题的资源包。
对于图标资源,如果使用SVG,可以通过CSS的fill或stroke直接改变颜色,不必为每个颜色准备单独文件。只有无法用CSS控制颜色的位图才需要两套文件。图标字体也可以跟随color属性自动切换,通常不需要额外资源。
最后,把主题相关变量集中到一个独立样式文件,比如theme.css,再通过构建工具按需合并,能降低维护成本。团队协作时要明确:任何新的颜色、边框、阴影、插图都必须通过变量接入,不允许在组件内部写死颜色值。这样才能保证深色模式持续可用,而不是上线一段时间后又出现浅色残留。