CSS 多主题方案 (Theming)
现代前端应用的主题切换方案,从 CSS 变量到 CSS-in-JS 的完整实践指南
目录
核心概念
什么是主题系统?
主题系统允许用户在不同的视觉风格(如亮色/暗色模式)之间切换,涉及:
| 维度 | 说明 |
|---|---|
| 颜色 | 背景、文字、边框、阴影 |
| 间距 | 内外边距、组件尺寸 |
| 字体 | 字号、字重、行高 |
| 圆角 | 边框弧度 |
CSS 变量方案
基础用法
css
/* 定义主题变量 */
:root {
/* 颜色 */
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-primary: #3b82f6;
--color-secondary: #64748b;
/* 间距 */
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
/* 圆角 */
--radius-sm: 4px;
--radius-md: 8px;
}
/* 暗色主题 */
[data-theme="dark"] {
--color-bg: #0f172a;
--color-text: #f1f5f9;
--color-primary: #60a5fa;
--color-secondary: #94a3b8;
}
/* 使用变量 */
.card {
background: var(--color-bg);
color: var(--color-text);
padding: var(--spacing-md);
border-radius: var(--radius-md);
}系统主题检测
css
/* 跟随系统主题 */
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0f172a;
--color-text: #f1f5f9;
}
}
@media (prefers-color-scheme: light) {
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
}
}主题切换实现
方案一:data-theme 属性切换
javascript
// 主题管理器
class ThemeManager {
constructor() {
this.STORAGE_KEY = 'theme-preference';
this.init();
}
init() {
// 优先级:用户设置 > 系统偏好 > 默认值
const stored = localStorage.getItem(this.STORAGE_KEY);
const systemPrefers = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
this.setTheme(stored || systemPrefers);
// 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
if (!localStorage.getItem(this.STORAGE_KEY)) {
this.setTheme(e.matches ? 'dark' : 'light');
}
});
}
setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem(this.STORAGE_KEY, theme);
}
toggle() {
const current = document.documentElement.getAttribute('data-theme');
this.setTheme(current === 'dark' ? 'light' : 'dark');
}
}
const themeManager = new ThemeManager();方案二:CSS 类切换
css
/* 默认亮色 */
.theme-light {
--color-bg: #ffffff;
--color-text: #1a1a1a;
}
/* 暗色 */
.theme-dark {
--color-bg: #0f172a;
--color-text: #f1f5f9;
}React Hook 封装
tsx
function useTheme() {
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
if (typeof window === 'undefined') return 'light';
return localStorage.getItem('theme') as 'light' | 'dark' || 'light';
});
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}, [theme]);
const toggle = useCallback(() => {
setTheme(prev => prev === 'dark' ? 'light' : 'dark');
}, []);
return { theme, setTheme, toggle };
}防止闪烁 (FOUC)
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>方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| CSS 变量 | 原生支持、性能好、运行时切换 | IE 不支持 | 现代项目首选 |
| CSS-in-JS | 类型安全、动态计算、与组件耦合 | 运行时开销、SSR 复杂 | React 生态 |
| 预处理器 | 兼容性好 | 编译时确定、切换需多套 CSS | 传统项目 |
| Tailwind | 类名驱动、与 dark: 前缀配合 | 学习成本 | Tailwind 项目 |
CSS-in-JS 示例 (styled-components)
tsx
import { ThemeProvider, createGlobalStyle } from 'styled-components';
const lightTheme = {
colors: { bg: '#ffffff', text: '#1a1a1a', primary: '#3b82f6' },
spacing: { sm: '8px', md: '16px' }
};
const darkTheme = {
colors: { bg: '#0f172a', text: '#f1f5f9', primary: '#60a5fa' },
spacing: { sm: '8px', md: '16px' }
};
const GlobalStyle = createGlobalStyle`
body {
background: ${props => props.theme.colors.bg};
color: ${props => props.theme.colors.text};
}
`;
function App() {
const [isDark, setIsDark] = useState(false);
return (
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
<GlobalStyle />
<button onClick={() => setIsDark(!isDark)}>Toggle</button>
</ThemeProvider>
);
}Tailwind 暗色模式
js
// tailwind.config.js
module.exports = {
darkMode: 'class', // 或 'media' 跟随系统
// ...
}html
<div class="bg-white dark:bg-slate-900 text-slate-900 dark:text-white">
自动切换
</div>设计 Token 规范
语义化命名
css
:root {
/* 不推荐:具体颜色命名 */
--blue-500: #3b82f6;
/* 推荐:语义化命名 */
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-success: #22c55e;
--color-error: #ef4444;
/* 层级化 */
--color-bg-base: #ffffff;
--color-bg-elevated: #f8fafc;
--color-bg-overlay: rgba(0, 0, 0, 0.5);
}多主题扩展
css
/* 品牌主题 */
[data-theme="brand-a"] {
--color-primary: #8b5cf6; /* 紫色系 */
}
[data-theme="brand-b"] {
--color-primary: #f97316; /* 橙色系 */
}高频面试题
Q1: CSS 变量和预处理器变量有什么区别?
| 特性 | CSS 变量 | 预处理器变量 |
|---|---|---|
| 解析时机 | 运行时 | 编译时 |
| 动态修改 | ✅ 支持 JS 修改 | ❌ 编译后固定 |
| 继承性 | ✅ 遵循 CSS 继承 | ❌ 无继承 |
| 作用域 | 选择器作用域 | 文件/块作用域 |
| 回退值 | var(--x, fallback) | 需手动处理 |
Q2: 如何避免主题切换时的闪烁?
- 阻塞脚本:在
<head>中同步读取并设置data-theme - CSS 匹配:使用
prefers-color-scheme作为默认值 - SSR:服务端根据 Cookie 注入正确的主题类名
Q3: 多主题系统的设计要点?
- 语义化 Token:用途命名而非颜色命名
- 层级结构:基础变量 → 语义变量 → 组件变量
- 对比度保证:确保文字与背景的对比度符合 WCAG 标准
- 过渡动画:添加
transition让切换更平滑
css
:root {
transition: background-color 0.3s, color 0.3s;
}Q4: CSS-in-JS 主题方案的运行时开销?
- 样式计算:每次渲染都需要计算样式对象
- CSS 注入:动态生成并插入
<style>标签 - 优化方向:静态提取(如 vanilla-extract)、原子化 CSS
最佳实践清单
- [ ] 使用 CSS 变量作为主题基础
- [ ] 支持跟随系统主题 (
prefers-color-scheme) - [ ] 持久化用户选择 (
localStorage) - [ ] 防止首屏闪烁 (同步脚本)
- [ ] 语义化 Token 命名
- [ ] 保证色彩对比度 (WCAG AA)
- [ ] 添加切换过渡动画