使用 CSS 变量构建主题切换器
样式表中散落的硬编码颜色值会让主题切换变得异常痛苦。改一个颜色,你就得在几十处声明中翻找。CSS 变量优雅地解决了这个问题——只需定义一次设计令牌(design tokens),然后通过修改根元素上的一个属性即可切换主题。
本文将带你一步步构建一个易于维护的主题切换器:它能自动遵循系统偏好、响应用户的手动选择,并在会话之间持久化这一选择。
关键要点
- CSS 自定义属性是动态的,可在运行时更新,是主题化场景下理想的设计令牌。
- 在根元素上使用
data-theme属性,提供了一种简洁且可扩展的方式来切换任意数量的主题。 prefers-color-scheme媒体查询负责处理系统默认设置,而 JavaScript 和localStorage则用于保存用户的手动选择。- 在
<head>中放置一段小型内联脚本,可避免页面加载时出现错误主题闪烁。 - 设置
color-scheme属性可确保浏览器原生 UI 元素与当前主题相匹配。
为什么 CSS 变量是主题化的理想基础
CSS 自定义属性是动态的。与编译为静态值的 Sass 变量不同,CSS 变量存在于浏览器中,可以在运行时被更新——通过媒体查询、通过 JavaScript,或通过父元素状态的变化。
这使它们成为理想的设计令牌。将表面色、文字色和强调色定义为 :root 上的变量,然后在整个样式表中引用这些变量。切换主题就变成了单次 DOM 修改,而不是替换整个样式表。
设置你的主题令牌
首先将浅色主题定义为默认值,然后使用 data-theme 属性选择器声明深色变体:
:root {
color-scheme: light;
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-primary: #302ae6;
--color-surface: #f4f4f4;
}
[data-theme="dark"] {
color-scheme: dark;
--color-bg: #161625;
--color-text: #e1e1ff;
--color-primary: #9a97f3;
--color-surface: #1e1e30;
}
color-scheme 属性值得加上。它会告诉浏览器当前处于哪种模式,这样原生 UI 元素——滚动条、表单输入框、聚焦轮廓等——就会自动与你的主题相匹配。
接下来在整个样式表中使用这些令牌:
body {
background-color: var(--color-bg);
color: var(--color-text);
}
使用 prefers-color-scheme 遵循系统偏好
系统偏好和手动主题切换解决的是不同的问题。prefers-color-scheme 处理的是自动场景——那些将操作系统设为深色模式、并期望网站随之变化的用户:
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
color-scheme: dark;
--color-bg: #161625;
--color-text: #e1e1ff;
--color-primary: #9a97f3;
--color-surface: #1e1e30;
}
}
:not([data-theme="light"]) 守卫条件确保用户的明确选择能够覆盖系统默认值。否则,该媒体查询会覆盖手动选择。
关于 light-dark() 的说明
light-dark() CSS 函数允许你在一行内声明两种主题的值:
:root {
color-scheme: light dark;
--color-bg: light-dark(#ffffff, #161625);
}
对于较为简单的令牌集合,以及只需要两种主题的场景,它非常优雅。但如果系统中有超过两种主题,或令牌涉及的不仅仅是颜色,基于属性选择器的方案会给你更多的控制权。
请注意,light-dark() 要求 color-scheme 同时设置两个值,函数才能正确解析。现代浏览器对它的支持已经相当不错,但相对较新(Chrome 123+、Safari 17.5+、Firefox 120+),所以在依赖它之前请先确认你的受众兼容性。
Discover how at OpenReplay.com.
添加 JavaScript 实现持久化的手动控制
系统偏好检测可以纯 CSS 实现,但记住用户的手动选择需要 JavaScript。下面是一个最小化的、可用于生产的实现:
const STORAGE_KEY = 'theme-preference';
const root = document.documentElement;
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function getSavedTheme() {
try {
return localStorage.getItem(STORAGE_KEY);
} catch {
return null;
}
}
function applyTheme(theme) {
root.setAttribute('data-theme', theme);
try {
localStorage.setItem(STORAGE_KEY, theme);
} catch {
// Storage unavailable — theme still applies for this session
}
}
// On load: respect saved preference, fall back to system
applyTheme(getSavedTheme() || getSystemTheme());
// Wire up your toggle button
document.getElementById('theme-toggle').addEventListener('click', () => {
const current = root.getAttribute('data-theme');
applyTheme(current === 'dark' ? 'light' : 'dark');
});
// Sync with OS changes when no manual preference is set
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!getSavedTheme()) {
root.setAttribute('data-theme', e.matches ? 'dark' : 'light');
}
});
围绕 localStorage 的 try/catch 块可以优雅地处理隐私浏览模式和存储配额错误——即便无法保存,主题在当前会话中仍然可以生效。
防止错误主题闪烁
如果你的 JavaScript 在页面渲染之后才加载,用户会短暂看到默认主题,然后才切换到正确的主题。可以通过在 <head> 中放置一段小型阻塞式内联脚本来解决:
<script>
try {
const saved = localStorage.getItem('theme-preference');
if (saved) {
document.documentElement.setAttribute('data-theme', saved);
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-theme', 'dark');
}
} catch {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-theme', 'dark');
}
}
</script>
这段脚本会在任何内容渲染之前同步执行,消除闪烁。请尽可能将它放在 <head> 的最前面,放在任何样式表之前,这样在 CSS 首次应用时该属性就已存在。
这一模式具备良好的可扩展性
CSS 变量让你的设计令牌和组件样式之间形成了清晰的分离。组件引用变量;主题定义变量。添加第三种主题——高对比度、品牌变体、季节性配色——只需在 CSS 中新增一个属性选择器块,并在切换逻辑中增加一个新选项。其他都无需改动。
结语
这里真正的价值不仅仅是深色模式——而是一种可随设计系统不断扩展、并保持可维护性的主题化架构。把 CSS 变量当作设计令牌,同时尊重系统偏好与用户偏好,并妥善处理各种边界情况(存储失败、渲染闪烁、操作系统级变更),你就能构建出一个既贴近平台原生体验、又易于扩展的切换器。这一模式体量很小,但每新增一个主题或组件,它的收益都会不断累积。
常见问题
两种方式都可行,但 data-theme 属性通常更简洁,因为它表达的是单一值,而非一组修饰符。class 更适合多个标志同时存在的组合状态。对于主题来说,通常同一时刻只有一个激活值,这与属性选择器的语义更为契合。
这种闪烁发生在浏览器已使用默认样式绘制完页面之后,JavaScript 才执行的情况下。解决办法是在 head 中放置一段小型同步内联脚本,从 localStorage 读取已保存的偏好,并在任何渲染发生之前就设置好 data-theme 属性。对于首次访问的用户,你还可以检查 prefers-color-scheme,从而让初次渲染立刻匹配系统主题。
可以。CSS 变量可以承载任何合法的 CSS 值,包括间距尺度、圆角半径、阴影、字体栈和动画时长。这让它们也适用于密度主题化(紧凑布局与舒适布局)、动效偏好,甚至排版变体。要广义地把变量视为设计令牌,而不仅仅是颜色引用。
它在现代浏览器中工作良好(Chrome 123+、Safari 17.5+、Firefox 120+),但在旧版本中缺乏支持。如果你的受众都在使用现代浏览器,这是一个简洁的选择。要获得更广泛的兼容性,data-theme 属性方案仍然更稳妥,而且它还能扩展到两种以上的主题,这是 light-dark() 自身做不到的。
Truly understand users experience
See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data. . Check our GitHub repo and join the thousands of developers in our community..