跳到主要内容

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: :roothtml 有什么区别?

答案

: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); /* 现代浏览器 */
}

相关链接