CSS 变量与主题切换:本站的深浅色是怎么做的
深浅色主题现在几乎是标配。用 CSS 变量做,整套配色只需要一份定义,切换时改一个属性即可。
一、定义变量
:root {
--bg: #f7f8fa;
--panel: #ffffff;
--text: #1b1f24;
--muted: #737b85;
--border: #e3e7ec;
--accent: #3b6cf6;
}
body {
background: var(--bg);
color: var(--text);
}要点:
- 变量名以
--开头,用var(--name)取值; - 定义在
:root上是全局的,也可以定义在某个元素上做局部覆盖; - 变量是继承的:子元素能拿到父元素的值,这正好用来做主题。
二、切换主题:覆盖变量
不需要写两套 CSS,只需要在切换时重新定义同一批变量:
:root { /* 浅色 */ }
[data-theme="dark"] {
--bg: #0c1016;
--panel: #151b23;
--text: #e7edf3;
--muted: #8b949e;
--border: #242c36;
--accent: #58a6ff;
}document.documentElement.setAttribute("data-theme", "dark");一行 JS 就切换了整个站点的配色。这就是 CSS 变量相对 Sass/Less 变量的核心优势:运行时可变。
三、带默认值与继承覆盖
var(--accent, #3b6cf6) /* 变量未定义时用默认值 */
/* 局部覆盖:某个卡片用不同强调色 */
.card-special { --accent: #e0483d; }四、跟随系统偏好
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
--bg: #0c1016;
/* ... 深色值 */
}
}:not([data-theme]) 保证用户手动选择后,系统偏好不再覆盖。
JS 侧也能读:
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
// 监听系统主题变化
window.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", e => console.log(e.matches ? "dark" : "light"));五、记住用户选择
const KEY = "baize-theme";
function applyTheme(t) {
document.documentElement.setAttribute("data-theme", t);
}
// 读取:用户选过就用选过的,否则跟随系统
let saved = null;
try { saved = localStorage.getItem(KEY); } catch (e) {}
if (!saved) {
saved = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
applyTheme(saved);
// 保存
btn.addEventListener("click", () => {
const next = document.documentElement.getAttribute("data-theme") === "dark" ? "light" : "dark";
applyTheme(next);
try { localStorage.setItem(KEY, next); } catch (e) {}
});
try/catch是必要的:file://协议下或隐私模式中,localStorage可能直接抛异常。本站要支持双击打开,所以必须包起来。
六、闪烁问题(FOUC)
问题:页面已经用浅色渲染完了,JS 才执行,主题突然跳一下。
解法:在 <head> 里用一小段内联脚本,在渲染前就设好属性。
<head>
<script>
(function () {
try {
var t = localStorage.getItem("baize-theme");
if (!t) {
t = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
document.documentElement.setAttribute("data-theme", t);
} catch (e) {}
})();
</script>
</head>这段脚本必须内联且同步,放在 CSS 之后、body 之前。它是全站唯一一段阻塞渲染的 JS,代价很小。
七、过渡动画要克制
切换主题时加过渡会更顺滑:
body {
transition: background-color .25s ease, color .25s ease;
}但别给所有元素加 transition: all——会导致切换时满屏元素都在动,很卡。只给背景和文字加即可。
另外注意:首屏加载时如果元素有 transition,会看到一次「从默认色过渡到主题色」的动画。解决办法是给 html 加个标记,加载完成后再启用过渡。
八、别忘了这些地方
做主题时容易漏掉的:
::selection { background: var(--accent); color: #fff; }
/* 滚动条 */
::-webkit-scrollbar { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border-strong); }
/* 表单控件 */
input, textarea {
background: var(--bg-soft);
color: var(--text);
border: 1px solid var(--border);
}
/* 图片在深色下的处理 */
[data-theme="dark"] img { opacity: .9; }还有 theme-color(移动端浏览器地址栏颜色):
<meta name="theme-color" content="#0c1016">九、本站的变量清单
:root {
/* 颜色 */
--bg: ...; --bg-soft: ...; --panel: ...;
--text: ...; --text-soft: ...; --muted: ...;
--border: ...; --border-strong: ...;
--accent: ...; --accent-2: ...; --accent-soft: ...;
/* 阴影 */
--shadow-sm: ...; --shadow: ...;
/* 尺寸 */
--radius: 14px; --radius-sm: 9px;
/* 代码高亮(深浅色各一套) */
--tok-keyword: ...; --tok-string: ...; --tok-comment: ...;
/* 字体 */
--font: ...; --mono: ...;
}把「会随主题变的东西」都做成变量,切换时只需维护一份深色覆盖块。本站的代码高亮配色也是这么做的。
十、color-mix 进一步简化
现代浏览器支持 color-mix,可以基于变量派生颜色:
background: color-mix(in srgb, var(--accent) 12%, transparent);
border-color: color-mix(in srgb, var(--accent) 45%, var(--border));好处是派生色自动跟随主题,不用在深色块里再定义一遍。
# 兼容性:Chrome 111+ / Safari 16.2+ / Firefox 113+
# 2026 年已经可以放心用了主题切换的本质就一句:把所有颜色抽成变量,切换时覆盖这一组变量。
