CSS 自定义属性(Custom Properties),也叫 CSS 变量,是 CSS3 引入的一项强大特性。与预处理器变量不同,CSS 变量是浏览器原生支持的、可以在运行时动态修改的值。这使得它们成为实现主题切换(如暗黑模式/明亮模式)的理想选择。
本文将深入讲解 CSS 变量的定义与使用、暗黑模式的实现方案、以及如何通过 JavaScript 实现动态主题切换。
一、CSS 变量基础
定义语法
CSS 变量使用 -- 前缀命名,通过 var() 函数引用:
:root {
/* 定义变量 */
--primary-color: #007bff;
--secondary-color: #6c757d;
--font-size-base: 16px;
--spacing-unit: 8px;
--border-radius: 4px;
--transition-speed: 0.3s;
}
/* 使用变量 */
.button {
background-color: var(--primary-color);
font-size: var(--font-size-base);
padding: var(--spacing-unit) calc(var(--spacing-unit) * 2);
border-radius: var(--border-radius);
transition: all var(--transition-speed) ease;
}
变量的作用域
CSS 变量遵循 CSS 的级联规则,子元素可以继承父元素的变量:
:root {
--text-color: #333;
}
.dark-section {
--text-color: #fff;
}
p {
color: var(--text-color);
/* 在 .dark-section 内的 p 元素颜色为 #fff */
/* 其他 p 元素颜色为 #333 */
}
变量默认值
当变量未定义时,var() 可以提供回退值:
.element {
/* 如果 --accent-color 未定义,使用 #ff6b35 */
color: var(--accent-color, #ff6b35);
/* 支持多层回退 */
color: var(--accent-color, var(--primary-color, #007bff));
}
变量与预处理器的区别
| 特性 | CSS 变量 | SCSS/Less 变量 |
|---|---|---|
| 作用域 | 遵循 CSS 级联和作用域 | 编译时确定,无作用域 |
| 运行时修改 | 可以通过 JS 动态修改 | 不支持,编译后为固定值 |
| 继承 | 可以继承 | 不可以 |
| 浏览器支持 | 现代浏览器均支持 | 需要预处理器编译 |
| 动态响应 | 可以响应媒体查询、JS 修改 | 不支持 |
| 条件逻辑 | 可以通过覆盖实现 | 需要 @if/@else |
二、使用 CSS 变量构建设计令牌(Design Tokens)
什么是设计令牌?
设计令牌是设计系统中最小的可复用单元,它们代表了设计决策的具体值,如颜色、字体、间距等。
设计令牌层级
:root {
/* ===== 全局令牌(Global Tokens)===== */
/* 原始值,不直接用于组件 */
--color-blue-50: #eff6ff;
--color-blue-100: #dbeafe;
--color-blue-500: #3b82f6;
--color-blue-600: #2563eb;
--color-blue-700: #1d4ed8;
--color-gray-50: #f9fafb;
--color-gray-100: #f3f4f6;
--color-gray-500: #6b7280;
--color-gray-700: #374151;
--color-gray-900: #111827;
--color-white: #ffffff;
--color-black: #000000;
--font-family-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-family-mono: 'Fira Code', 'Consolas', monospace;
--font-size-xs: 0.75rem; /* 12px */
--font-size-sm: 0.875rem; /* 14px */
--font-size-base: 1rem; /* 16px */
--font-size-lg: 1.125rem; /* 18px */
--font-size-xl: 1.25rem; /* 20px */
--font-size-2xl: 1.5rem; /* 24px */
--font-size-3xl: 1.875rem; /* 30px */
--spacing-1: 0.25rem; /* 4px */
--spacing-2: 0.5rem; /* 8px */
--spacing-3: 0.75rem; /* 12px */
--spacing-4: 1rem; /* 16px */
--spacing-5: 1.25rem; /* 20px */
--spacing-6: 1.5rem; /* 24px */
--spacing-8: 2rem; /* 32px */
/* ===== 语义令牌(Semantic Tokens)===== */
/* 有明确含义,直接用于组件 */
--color-primary: var(--color-blue-600);
--color-primary-hover: var(--color-blue-700);
--color-primary-light: var(--color-blue-50);
--color-text: var(--color-gray-900);
--color-text-secondary: var(--color-gray-500);
--color-text-inverse: var(--color-white);
--color-bg: var(--color-white);
--color-bg-secondary: var(--color-gray-50);
--color-bg-tertiary: var(--color-gray-100);
--color-border: var(--color-gray-100);
--color-border-strong: var(--color-gray-500);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.15);
}
三、暗黑模式实现
方案一:使用 prefers-color-scheme 媒体查询
这是最简单的方式,跟随用户系统的主题偏好:
:root {
--color-text: #111827;
--color-bg: #ffffff;
--color-bg-secondary: #f9fafb;
--color-border: #e5e7eb;
--color-primary: #2563eb;
}
/* 用户系统设置为暗黑模式时 */
@media (prefers-color-scheme: dark) {
:root {
--color-text: #e5e7eb;
--color-bg: #111827;
--color-bg-secondary: #1f2937;
--color-border: #374151;
--color-primary: #3b82f6;
}
}
/* 使用变量 */
body {
background-color: var(--color-bg);
color: var(--color-text);
}
.card {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
}
方案二:手动切换主题(类名方案)
通过给 <html> 或 <body> 添加类名来切换主题:
/* 默认(明亮主题) */
:root {
--color-text: #111827;
--color-text-secondary: #6b7280;
--color-bg: #ffffff;
--color-bg-secondary: #f9fafb;
--color-bg-tertiary: #f3f4f6;
--color-border: #e5e7eb;
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--shadow-color: rgba(0, 0, 0, 0.1);
}
/* 暗黑主题 */
[data-theme="dark"] {
--color-text: #e5e7eb;
--color-text-secondary: #9ca3af;
--color-bg: #111827;
--color-bg-secondary: #1f2937;
--color-bg-tertiary: #374151;
--color-border: #4b5563;
--color-primary: #3b82f6;
--color-primary-hover: #60a5fa;
--shadow-color: rgba(0, 0, 0, 0.3);
}
方案三:多主题支持
/* 明亮主题(默认) */
:root,
[data-theme="light"] {
--color-text: #111827;
--color-bg: #ffffff;
--color-primary: #2563eb;
--color-success: #16a34a;
--color-warning: #d97706;
--color-danger: #dc2626;
}
/* 暗黑主题 */
[data-theme="dark"] {
--color-text: #e5e7eb;
--color-bg: #111827;
--color-primary: #3b82f6;
--color-success: #22c55e;
--color-warning: #f59e0b;
--color-danger: #ef4444;
}
/* 蓝色主题(品牌主题) */
[data-theme="ocean"] {
--color-text: #1e3a5f;
--color-bg: #f0f7ff;
--color-primary: #0369a1;
--color-success: #0d9488;
--color-warning: #ca8a04;
--color-danger: #be123c;
}
/* 护眼主题 */
[data-theme="nature"] {
--color-text: #2d3a2e;
--color-bg: #f5f7f0;
--color-primary: #4a7c59;
--color-success: #2d6a4f;
--color-warning: #bc6c25;
--color-danger: #9b2226;
}
四、JavaScript 动态主题切换
基础主题切换器
// 主题切换逻辑
class ThemeSwitcher {
constructor(options = {}) {
this.storageKey = options.storageKey || 'theme-preference';
this.defaultTheme = options.defaultTheme || 'light';
this.themes = options.themes || ['light', 'dark'];
this.root = document.documentElement;
this.init();
}
init() {
// 1. 获取存储的主题偏好
const stored = localStorage.getItem(this.storageKey);
// 2. 如果有存储值,使用存储值;否则使用系统偏好
if (stored) {
this.setTheme(stored, false);
} else {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
this.setTheme(prefersDark ? 'dark' : 'light', false);
}
// 3. 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
if (!localStorage.getItem(this.storageKey)) {
this.setTheme(e.matches ? 'dark' : 'light', false);
}
});
}
setTheme(theme, save = true) {
if (!this.themes.includes(theme)) return;
this.root.setAttribute('data-theme', theme);
this.currentTheme = theme;
if (save) {
localStorage.setItem(this.storageKey, theme);
}
}
toggle() {
const nextTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
this.setTheme(nextTheme);
}
getTheme() {
return this.currentTheme;
}
}
// 使用
const themeSwitcher = new ThemeSwitcher({
defaultTheme: 'light',
themes: ['light', 'dark', 'ocean', 'nature']
});
// 绑定按钮
document.getElementById('theme-toggle')
.addEventListener('click', () => themeSwitcher.toggle());
避免闪烁(FOUC)
主题切换时常见的"闪烁"问题:页面加载时先显示默认主题,再切换到用户偏好主题。解决方案是在 <head> 中内联以下脚本:
<!DOCTYPE html>
<html>
<head>
<script>
// 尽早设置主题,避免闪烁
(function() {
const theme = localStorage.getItem('theme-preference')
|| (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- 页面内容 -->
</body>
</html>
添加切换动画
/* 主题切换时的过渡效果 */
[data-theme] * {
transition: background-color 0.3s ease,
color 0.3s ease,
border-color 0.3s ease,
box-shadow 0.3s ease;
}
/* 排除不需要过渡的元素 */
[data-theme] script,
[data-theme] iframe {
transition: none;
}
五、主题切换的 HTML 实现示例
<div class="theme-selector">
<button
class="theme-btn"
data-theme="light"
aria-label="切换到明亮模式"
title="明亮模式"
>
☀️ 明亮
</button>
<button
class="theme-btn"
data-theme="dark"
aria-label="切换到暗黑模式"
title="暗黑模式"
>
🌙 暗黑
</button>
<button
class="theme-btn"
data-theme="ocean"
aria-label="切换到海洋主题"
title="海洋主题"
>
🌊 海洋
</button>
</div>
<script>
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.addEventListener('click', () => {
const theme = btn.getAttribute('data-theme');
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme-preference', theme);
});
});
</script>
六、CSS 变量使用注意事项
1. 变量命名规范
/* 不好的命名 */
:root {
--c1: #333; /* 含义不明 */
--blue: blue; /* 过于笼统 */
--fs: 16px; /* 缩写不清晰 */
}
/* 好的命名 */
:root {
--color-text-primary: #333;
--color-brand-blue: #2563eb;
--font-size-base: 16px;
}
2. 常见陷阱
/* 陷阱 1:变量在 calc() 中的使用需要空格 */
.element {
/* 错误 */
width: calc(var(--base-width) * 2);
/* 正确 */
width: calc(var(--base-width) * 2);
/* 错误:缺少单位 */
--gap: 16;
margin: var(--gap)px; /* 无效 */
/* 正确 */
--gap: 16px;
margin: var(--gap);
}
/* 陷阱 2:变量值不能包含 !important */
:root {
/* 错误 */
--color: red !important;
/* 正确 */
--color: red;
}
/* 陷阱 3:变量回退值中使用逗号 */
.element {
/* 如果变量值中包含逗号,需要特殊处理 */
font-family: var(--font-family, 'Helvetica Neue', Arial, sans-serif);
/* 这里回退值实际上会被解析为多个值,需要加引号 */
}
3. 浏览器兼容性
| 特性 | Chrome | Firefox | Safari | Edge | IE |
|---|---|---|---|---|---|
| CSS 变量 | 49+ | 31+ | 9.1+ | 15+ | 不支持 |
@property | 85+ | 128+ | 15+ | 85+ | 不支持 |
对于不支持 CSS 变量的浏览器,需要提供回退值:
.element {
/* 回退值写在 var() 前面 */
color: #333;
color: var(--color-text, #333);
}
七、完整的主题系统示例
/* ===== 完整主题系统 ===== */
/* 设计令牌 - 全局 */
:root {
/* 间距 */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
--space-lg: 24px;
--space-xl: 32px;
/* 圆角 */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 16px;
--radius-full: 9999px;
/* 字体 */
--font-sans: system-ui, -apple-system, sans-serif;
--font-mono: ui-monospace, monospace;
/* 过渡 */
--duration-fast: 150ms;
--duration-normal: 300ms;
--duration-slow: 500ms;
}
/* 语义令牌 - 明亮主题 */
:root,
[data-theme="light"] {
--color-bg: #ffffff;
--color-bg-soft: #f9fafb;
--color-bg-muted: #f3f4f6;
--color-text: #111827;
--color-text-subtle: #6b7280;
--color-border: #e5e7eb;
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-shadow: rgba(0, 0, 0, 0.1);
}
/* 语义令牌 - 暗黑主题 */
[data-theme="dark"] {
--color-bg: #0f172a;
--color-bg-soft: #1e293b;
--color-bg-muted: #334155;
--color-text: #f1f5f9;
--color-text-subtle: #94a3b8;
--color-border: #475569;
--color-primary: #3b82f6;
--color-primary-hover: #60a5fa;
--color-shadow: rgba(0, 0, 0, 0.4);
}
/* 组件样式 - 完全使用变量 */
.card {
background: var(--color-bg-soft);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-lg);
box-shadow: 0 2px 4px var(--color-shadow);
transition: background-color var(--duration-normal),
color var(--duration-normal),
border-color var(--duration-normal);
}
.btn-primary {
background: var(--color-primary);
color: white;
border: none;
border-radius: var(--radius-sm);
padding: var(--space-sm) var(--space-md);
font-size: var(--font-size-base, 16px);
cursor: pointer;
}
.btn-primary:hover {
background: var(--color-primary-hover);
}
总结
CSS 变量为前端主题系统带来了革命性的变化:
- 定义变量:使用
:root定义全局变量,使用作用域覆盖实现局部变量 - 设计令牌:分层管理全局令牌和语义令牌
- 暗黑模式:通过
prefers-color-scheme、data-theme属性、或类名方案实现 - 动态切换:结合 JavaScript 实现用户可控的主题切换
- 避免闪烁:在
<head>中尽早设置主题属性
通过合理运用 CSS 变量,你可以构建一个灵活、可维护、用户友好的主题系统。