CSS 变量与主题切换
问题
什么是 CSS 自定义属性(CSS 变量)?如何用 CSS 变量实现主题切换和暗色模式?
答案
CSS 自定义属性基础
CSS 自定义属性(CSS Custom Properties,俗称 CSS 变量)以 -- 开头,通过 var() 函数引用。
基本用法
:root {
/* 定义变量 */
--primary-color: #3b82f6;
--font-size-base: 16px;
--spacing-md: 16px;
--border-radius: 8px;
}
.button {
/* 使用变量 */
background: var(--primary-color);
font-size: var(--font-size-base);
padding: var(--spacing-md);
border-radius: var(--border-radius);
}
var() 回退值
.box {
/* 如果 --color 未定义,使用 #333 */
color: var(--color, #333);
/* 多层回退 */
color: var(--theme-color, var(--primary-color, blue));
/* 回退值可以是任意合法 CSS 值 */
padding: var(--spacing, 8px 16px);
}
CSS 变量的特性
1. 作用域与继承
CSS 变量遵循级联和继承规则:
:root {
--color: blue; /* 全局变量 */
}
.card {
--color: red; /* 局部变量,仅在 .card 及其子元素内生效 */
}
.card .title {
color: var(--color); /* red(继承自 .card) */
}
.other {
color: var(--color); /* blue(继承自 :root) */
}
2. 动态性(与预处理器变量的核心区别)
CSS 变量是运行时的,可以通过 JS 动态修改、被媒体查询覆盖:
// JS 动态修改 CSS 变量
const root = document.documentElement;
root.style.setProperty('--primary-color', '#10b981');
// 读取 CSS 变量
const color = getComputedStyle(root).getPropertyValue('--primary-color');
/* 媒体查询中覆盖 */
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1a1a1a;
--text-color: #e0e0e0;
}
}
3. 与 calc() 配合
:root {
--base-size: 8px;
}
.box {
padding: calc(var(--base-size) * 2); /* 16px */
margin: calc(var(--base-size) * 3); /* 24px */
font-size: calc(var(--base-size) * 1.75); /* 14px */
}
4. 无单位变量的拼接
:root {
--columns: 4;
}
.grid {
/* ❌ 不能直接拼接单位 */
grid-template-columns: repeat(var(--columns)px, 1fr); /* 无效 */
/* ✅ 需要用 calc() 乘以 1 来添加单位 */
width: calc(var(--columns) * 100px);
/* ✅ repeat() 接受无单位数字 */
grid-template-columns: repeat(var(--columns), 1fr); /* 有效 */
}
主题切换实现
方案 1:CSS 类名切换(推荐)
定义主题变量
/* 浅色主题(默认) */
:root,
[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--text-primary: #1a1a1a;
--text-secondary: #666666;
--border-color: #e5e5e5;
--primary: #3b82f6;
--primary-hover: #2563eb;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 暗色主题 */
[data-theme="dark"] {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--border-color: #404040;
--primary: #60a5fa;
--primary-hover: #93c5fd;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
使用主题变量
body {
background: var(--bg-primary);
color: var(--text-primary);
}
.card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
box-shadow: var(--shadow);
}
.button {
background: var(--primary);
color: white;
}
.button:hover {
background: var(--primary-hover);
}
主题切换逻辑
type Theme = 'light' | 'dark' | 'system';
function setTheme(theme: Theme): void {
if (theme === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.dataset.theme = prefersDark ? 'dark' : 'light';
} else {
document.documentElement.dataset.theme = theme;
}
localStorage.setItem('theme', theme);
}
// 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e: MediaQueryListEvent) => {
const saved = localStorage.getItem('theme') as Theme;
if (saved === 'system' || !saved) {
document.documentElement.dataset.theme = e.matches ? 'dark' : 'light';
}
});
// 初始化
function initTheme(): void {
const saved = (localStorage.getItem('theme') as Theme) || 'system';
setTheme(saved);
}
方案 2:prefers-color-scheme(跟随系统)
:root {
--bg: #ffffff;
--text: #1a1a1a;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
--text: #e0e0e0;
}
}
方案 3:color-scheme 属性
:root {
color-scheme: light dark; /* 声明支持两种模式 */
}
color-scheme 会让浏览器原生控件(滚动条、表单元素、选择框)自动适配暗色模式。
Design Token 系统
将设计变量组织为层次化的 Token:
Design Token 示例
:root {
/* === Primitive Tokens(原始值) === */
--color-blue-50: #eff6ff;
--color-blue-500: #3b82f6;
--color-blue-600: #2563eb;
--color-gray-50: #f9fafb;
--color-gray-900: #111827;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--spacing-1: 0.25rem;
--spacing-2: 0.5rem;
--spacing-4: 1rem;
--spacing-6: 1.5rem;
/* === Semantic Tokens(语义化) === */
--color-primary: var(--color-blue-500);
--color-primary-hover: var(--color-blue-600);
--color-bg: var(--color-gray-50);
--color-text: var(--color-gray-900);
--font-size-body: var(--font-size-base);
--font-size-heading: var(--font-size-lg);
}
[data-theme="dark"] {
/* 暗色模式只需覆盖语义 Token */
--color-bg: #0f172a;
--color-text: #f1f5f9;
--color-primary: #60a5fa;
--color-primary-hover: #93c5fd;
}
分层的好处
通过 Primitive → Semantic 的分层:
- 换主题只需修改 Semantic Token 的映射
- 新增主题(如品牌主题)非常方便
- 与 Figma Design Token 插件或 Style Dictionary 等工具配合使用
过渡动画
主题切换过渡
/* 全局颜色过渡 */
* {
transition: background-color 0.3s ease,
color 0.3s ease,
border-color 0.3s ease,
box-shadow 0.3s ease;
}
/* 注意:全局 transition 可能导致性能问题 */
/* 更精确的做法:只在需要的元素上添加 */
body, .card, .button, .nav {
transition: background-color 0.3s ease, color 0.3s ease;
}
常见面试问题
Q1: CSS 变量和 Sass/Less 变量的区别?
答案:
| 特性 | CSS 变量 | Sass/Less 变量 |
|---|---|---|
| 运行时机 | 运行时(浏览器中) | 编译时(构建阶段) |
| 动态修改 | ✅ JS 可修改 | ❌ 编译后消失 |
| 作用域 | CSS 级联继承 | 块级作用域 |
| 媒体查询 | ✅ 可在 @media 中覆盖 | ❌ |
| 浏览器支持 | 现代浏览器 | 需要编译器 |
| 使用场景 | 主题切换、动态样式 | 复用、计算 |
CSS 变量和预处理器变量可以配合使用,互不冲突。
Q2: 如何用纯 CSS 实现暗色模式?
答案:
:root {
color-scheme: light dark;
--bg: #fff;
--text: #333;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
--text: #e0e0e0;
}
}
body {
background: var(--bg);
color: var(--text);
}
加上 color-scheme: light dark 让表单元素、滚动条等自动适配。
Q3: CSS 变量可以做动画吗?
答案:
默认不行,因为浏览器不知道 CSS 变量的类型。但可以用 @property 注册后实现:
@property --angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.box {
--angle: 0deg;
background: linear-gradient(var(--angle), red, blue);
transition: --angle 0.5s ease;
}
.box:hover {
--angle: 180deg; /* 渐变角度平滑过渡! */
}
@property 让浏览器知道变量的类型(<angle>、<color>、<length> 等),从而可以进行插值动画。
Q4: CSS 变量的作用域是怎么工作的?
答案:
CSS 变量遵循级联规则和继承:
:root { --color: blue; } /* 全局 */
.parent { --color: red; } /* 局部 */
.parent .child { color: var(--color); } /* red(从 .parent 继承) */
.other { color: var(--color); } /* blue(从 :root 继承) */
- 在哪个选择器中定义,就在哪个范围内生效
- 子元素会继承父元素的 CSS 变量
- 更具体的选择器中的变量会覆盖更宽泛的
Q5: 如何避免主题切换时的闪烁(FOUC)?
答案:
在 <head> 中用同步脚本提前设置主题,避免页面先以默认主题渲染再切换:
<head>
<script>
// 在 CSS 加载前执行,避免闪烁
(function() {
var theme = localStorage.getItem('theme') || 'system';
if (theme === 'system') {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
document.documentElement.dataset.theme = theme;
})();
</script>
<link rel="stylesheet" href="styles.css">
</head>
Q6: :root 和 html 有什么区别?
答案:
:root 是一个伪类,在 HTML 中等价于 html,但优先级更高:
html的优先级:(0,0,0,1):root的优先级:(0,0,1,0)
在 CSS 变量中习惯用 :root 定义全局变量,因为语义更明确——"定义在文档根元素上"。
Q7: 多主题(不只是暗色模式)如何实现?
答案:
/* 默认主题 */
:root, [data-theme="default"] {
--primary: #3b82f6;
--bg: #ffffff;
}
/* 暗色主题 */
[data-theme="dark"] {
--primary: #60a5fa;
--bg: #1a1a1a;
}
/* 品牌主题 A */
[data-theme="brand-a"] {
--primary: #10b981;
--bg: #f0fdf4;
}
/* 品牌主题 B */
[data-theme="brand-b"] {
--primary: #f59e0b;
--bg: #fffbeb;
}
function switchTheme(theme: string): void {
document.documentElement.dataset.theme = theme;
localStorage.setItem('theme', theme);
}
Q8: CSS 变量的浏览器兼容性如何?
答案:
CSS 自定义属性(var())支持所有现代浏览器(Chrome 49+、Firefox 31+、Safari 9.1+、Edge 15+)。不支持 IE。
兼容方案:
.button {
background: #3b82f6; /* 回退值(IE) */
background: var(--primary, #3b82f6); /* 现代浏览器 */
}