学完这篇,你能用一套 CSS 变量把整站配色抽成可开关的主题,实现明暗主题一键切换、跟随系统、刷新不闪白。
第一步:把颜色抽成变量字典
打开你的主样式文件(比如 style.css),在文件最顶部写一段 :root 声明。:root 就是 <html>,在这里定义的变量全局都能读到。
:root {
--c-bg: #ffffff;
--c-text: #1f2329;
--c-border: #e5e6eb;
--c-primary: #3b82f6;
--radius: 8px;
}
变量名必须以两个短横线 -- 开头,区分大小写。命名建议用「语义 + 用途」,--c-text 比 --gray-1 好,因为换主题时你改的是值,不是名字。
第二步:把写死的颜色换成 var()
接下来把样式里所有硬编码的色值替换掉,用 var() 取用:
body { background: var(--c-bg); color: var(--c-text); }
.card { border: 1px solid var(--c-border); border-radius: var(--radius); }
.btn { background: var(--c-primary); color: #fff; }
var() 支持兜底值:var(--c-primary, #3b82f6),变量没定义时用第二个参数,适合做组件库的默认样式。
这一步做完,页面外观应该和之前一模一样——说明抽取没问题。
第三步:用属性选择器写第二套主题
关键来了:暗色主题不需要重写任何组件样式,只要重新赋值同一批变量。
html[data-theme="dark"] {
--c-bg: #121212;
--c-text: #e6e6e6;
--c-border: #2a2a2a;
--c-primary: #60a5fa;
}
因为组件用的是 var(--c-bg),变量在 html 上被覆盖后,所有后代元素自动跟着变。这就是 CSS 变量的妙处:切换点只有一处,改的是数据不是规则。
顺带一个野路子——局部换肤。给某个卡片加内联样式 <div class="card" style="--c-primary: #f5222d">,这个卡片和它的子元素就用红色主题,不影响全局。
第四步:按钮切换 + localStorage 记忆
<button id="themeBtn">切换主题</button>
<script>
const root = document.documentElement;
const KEY = 'theme';
function apply(t) {
root.setAttribute('data-theme', t); // 换成 setAttribute 才会覆盖 :root 的变量
localStorage.setItem(KEY, t);
}
themeBtn.onclick = () => {
apply(root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
};
</script>
注意:这里必须用 setAttribute('data-theme', ...),而不是 root.style.setProperty。内联样式优先级最高,用 setProperty 之后浏览器会写出一堆行内变量,后面想整体切主题反而被自己挡住。
第五步:防闪白,脚本要放在 head 里
如果切换逻辑写在页面底部,暗色用户刷新时会先看到一瞬白屏再变黑。解决办法是在 <head> 里内联一段同步脚本(不能加 defer):
<script>
(function () {
var t = localStorage.getItem('theme');
if (!t) t = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', t);
})();
</script>
这段先于 body 渲染执行,首屏就是正确颜色。
如果想让用户有三档选择(浅色 / 深色 / 跟随系统),把「跟随」实现成不给 html 设 data-theme,并用媒体查询兜底:
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--c-bg: #121212; --c-text: #e6e6e6; --c-border: #2a2a2a;
}
}
第六步:避坑清单
注意:CSS 变量不能用在媒体查询条件里,@media (min-width: var(--bp)) 是无效的;也不能拼接属性名或选择器,只能作为属性值整体使用。断点值该写死还得写死。
注意:别在 * 上挂 transition: all .3s。切主题时全站元素同时做过渡,滚动和动画会明显卡顿。正确做法是把过渡写在具体属性上,例如 .card { transition: background-color .2s, border-color .2s; }。
注意:图片、阴影这类无法用颜色变量表达的资源,暗色下建议加一层遮罩或降低 opacity,不要试图用变量拼 url() 路径。
注意:旧版浏览器(IE 全系)不支持自定义属性,会直接忽略声明导致颜色丢失。如果必须兼容,给关键属性写一条兜底声明:background: #fff; background: var(--c-bg);。
小结
- 变量声明在
:root,名字取语义而非具体色值。
- 组件样式只写
var(),不写死颜色。
- 主题切换本质是给
html 挂 data-theme,用属性选择器覆盖变量,不碰任何组件规则。
setAttribute 而非内联样式,避免优先级打架。
- 读主题的脚本内联在
<head>,消除首屏闪白。
- 过渡只加在具体属性上,媒体查询和属性名里用不了变量。