CSS 变量体系与主题系统深度指南
深入探索 CSS 自定义属性的强大功能,构建可维护、可扩展的主题系统。从基础用法到高级技巧,从设计令牌到动态主题,全面掌握现代 CSS 变量技术。
📋 本章内容
🎯 学习目标
- 深入理解 CSS 变量的工作原理和最佳实践
- 掌握设计令牌系统的构建方法
- 学会实现复杂的动态主题切换
- 了解 CSS 变量的高级用法和性能优化
- 建立可维护的主题系统架构
🔧 CSS 变量基础与原理
变量声明与作用域
CSS 变量(自定义属性)遵循 CSS 的层叠规则,具有明确的作用域:
/* 全局变量 */
:root {
--primary-color: #42b883;
--secondary-color: #35495e;
--font-size-base: 16px;
--spacing-unit: 8px;
}
/* 局部变量 */
.component {
--component-bg: #ffffff;
--component-padding: var(--spacing-unit) * 2;
}
/* 变量继承 */
.component--variant {
--component-bg: var(--primary-color);
--component-color: #ffffff;
}
变量类型与计算
/* 不同类型的变量 */
:root {
/* 颜色值 */
--color-primary: #42b883;
--color-primary-rgb: 66, 184, 131;
--color-primary-hsl: 150, 50%, 50%;
/* 数值 */
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
/* 字符串 */
--font-family-primary: 'Inter', sans-serif;
--font-family-mono: 'Fira Code', monospace;
/* 复杂值 */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--gradient-primary: linear-gradient(135deg, var(--color-primary), #2c3e50);
}
/* 使用 calc() 进行计算 */
.component {
--component-width: calc(100% - var(--spacing-md) * 2);
--component-height: calc(var(--spacing-lg) * 3);
--component-font-size: calc(var(--font-size-base) * 1.125);
}
变量回退与默认值
/* 变量回退 */
.component {
/* 如果 --custom-color 不存在,使用 --primary-color */
color: var(--custom-color, var(--primary-color));
/* 多重回退 */
background: var(--custom-bg, var(--primary-color, #42b883));
/* 复杂回退 */
box-shadow: var(--custom-shadow, var(--shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1)));
}
/* 条件变量 */
.theme-dark {
--text-color: #ffffff;
--bg-color: #1a1a1a;
}
.theme-light {
--text-color: #333333;
--bg-color: #ffffff;
}
/* 使用环境变量 */
.component {
color: var(--text-color, #333333);
background: var(--bg-color, #ffffff);
}
🎨 设计令牌系统构建
分层设计令牌架构
/* 1. 基础令牌层 - 原始值 */
:root {
/* 颜色基础值 */
--color-blue-50: #eff6ff;
--color-blue-100: #dbeafe;
--color-blue-200: #bfdbfe;
--color-blue-300: #93c5fd;
--color-blue-400: #60a5fa;
--color-blue-500: #3b82f6;
--color-blue-600: #2563eb;
--color-blue-700: #1d4ed8;
--color-blue-800: #1e40af;
--color-blue-900: #1e3a8a;
/* 间距基础值 */
--space-0: 0;
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-5: 1.25rem; /* 20px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-10: 2.5rem; /* 40px */
--space-12: 3rem; /* 48px */
--space-16: 4rem; /* 64px */
--space-20: 5rem; /* 80px */
/* 字体基础值 */
--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 */
--font-size-4xl: 2.25rem; /* 36px */
/* 圆角基础值 */
--radius-none: 0;
--radius-sm: 0.125rem; /* 2px */
--radius-md: 0.375rem; /* 6px */
--radius-lg: 0.5rem; /* 8px */
--radius-xl: 0.75rem; /* 12px */
--radius-2xl: 1rem; /* 16px */
--radius-full: 9999px;
}
/* 2. 语义令牌层 - 有意义的名称 */
:root {
/* 语义颜色 */
--color-primary: var(--color-blue-500);
--color-primary-hover: var(--color-blue-600);
--color-primary-active: var(--color-blue-700);
--color-primary-light: var(--color-blue-100);
--color-primary-dark: var(--color-blue-900);
--color-success: #10b981;
--color-warning: #f59e0b;
--color-error: #ef4444;
--color-info: var(--color-blue-500);
/* 语义间距 */
--spacing-xs: var(--space-1);
--spacing-sm: var(--space-2);
--spacing-md: var(--space-4);
--spacing-lg: var(--space-6);
--spacing-xl: var(--space-8);
/* 语义字体 */
--font-size-caption: var(--font-size-xs);
--font-size-body: var(--font-size-base);
--font-size-title: var(--font-size-xl);
--font-size-heading: var(--font-size-2xl);
}
/* 3. 组件令牌层 - 组件特定值 */
:root {
/* 按钮组件令牌 */
--btn-padding-x: var(--spacing-md);
--btn-padding-y: var(--spacing-sm);
--btn-font-size: var(--font-size-sm);
--btn-font-weight: 500;
--btn-border-radius: var(--radius-md);
--btn-border-width: 1px;
--btn-transition: all 0.2s ease;
/* 卡片组件令牌 */
--card-padding: var(--spacing-lg);
--card-border-radius: var(--radius-lg);
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--card-border: 1px solid rgba(0, 0, 0, 0.1);
/* 表单组件令牌 */
--input-padding-x: var(--spacing-sm);
--input-padding-y: var(--spacing-sm);
--input-border-radius: var(--radius-md);
--input-border-width: 1px;
--input-font-size: var(--font-size-base);
}
响应式设计令牌
/* 响应式间距 */
:root {
--spacing-responsive-sm: clamp(var(--space-2), 2vw, var(--space-4));
--spacing-responsive-md: clamp(var(--space-4), 4vw, var(--space-8));
--spacing-responsive-lg: clamp(var(--space-6), 6vw, var(--space-12));
}
/* 响应式字体 */
:root {
--font-size-responsive-sm: clamp(var(--font-size-sm), 2.5vw, var(--font-size-base));
--font-size-responsive-md: clamp(var(--font-size-base), 3vw, var(--font-size-lg));
--font-size-responsive-lg: clamp(var(--font-size-lg), 4vw, var(--font-size-xl));
}
/* 响应式圆角 */
:root {
--radius-responsive-sm: clamp(var(--radius-sm), 1vw, var(--radius-md));
--radius-responsive-md: clamp(var(--radius-md), 2vw, var(--radius-lg));
--radius-responsive-lg: clamp(var(--radius-lg), 3vw, var(--radius-xl));
}
/* 使用示例 */
.responsive-component {
padding: var(--spacing-responsive-md);
font-size: var(--font-size-responsive-md);
border-radius: var(--radius-responsive-md);
}
组件变体系统
/* 基础组件样式 */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--btn-padding-y) var(--btn-padding-x);
font-size: var(--btn-font-size);
font-weight: var(--btn-font-weight);
border-radius: var(--btn-border-radius);
border: var(--btn-border-width) solid transparent;
transition: var(--btn-transition);
cursor: pointer;
text-decoration: none;
}
/* 按钮变体 */
.btn--primary {
--btn-bg: var(--color-primary);
--btn-color: #ffffff;
--btn-border-color: var(--color-primary);
--btn-hover-bg: var(--color-primary-hover);
--btn-active-bg: var(--color-primary-active);
background-color: var(--btn-bg);
color: var(--btn-color);
border-color: var(--btn-border-color);
}
.btn--primary:hover {
background-color: var(--btn-hover-bg);
border-color: var(--btn-hover-bg);
}
.btn--primary:active {
background-color: var(--btn-active-bg);
border-color: var(--btn-active-bg);
}
.btn--secondary {
--btn-bg: transparent;
--btn-color: var(--color-primary);
--btn-border-color: var(--color-primary);
--btn-hover-bg: var(--color-primary-light);
background-color: var(--btn-bg);
color: var(--btn-color);
border-color: var(--btn-border-color);
}
.btn--secondary:hover {
background-color: var(--btn-hover-bg);
}
/* 按钮尺寸 */
.btn--sm {
--btn-padding-x: var(--spacing-sm);
--btn-padding-y: var(--spacing-xs);
--btn-font-size: var(--font-size-xs);
}
.btn--lg {
--btn-padding-x: var(--spacing-lg);
--btn-padding-y: var(--spacing-md);
--btn-font-size: var(--font-size-lg);
}
🌓 动态主题切换实现
基础主题切换
/* 浅色主题 */
:root {
--theme-bg-primary: #ffffff;
--theme-bg-secondary: #f8f9fa;
--theme-bg-tertiary: #e9ecef;
--theme-text-primary: #212529;
--theme-text-secondary: #6c757d;
--theme-text-tertiary: #adb5bd;
--theme-border-primary: #dee2e6;
--theme-border-secondary: #e9ecef;
--theme-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--theme-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--theme-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
}
/* 深色主题 */
:root[data-theme="dark"] {
--theme-bg-primary: #0d1117;
--theme-bg-secondary: #161b22;
--theme-bg-tertiary: #21262d;
--theme-text-primary: #f0f6fc;
--theme-text-secondary: #8b949e;
--theme-text-tertiary: #6e7681;
--theme-border-primary: #30363d;
--theme-border-secondary: #21262d;
--theme-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--theme-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
--theme-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
}
/* 高对比度主题 */
:root[data-theme="high-contrast"] {
--theme-bg-primary: #000000;
--theme-bg-secondary: #ffffff;
--theme-bg-tertiary: #ffffff;
--theme-text-primary: #ffffff;
--theme-text-secondary: #000000;
--theme-text-tertiary: #000000;
--theme-border-primary: #ffffff;
--theme-border-secondary: #000000;
--theme-shadow-sm: 0 1px 2px rgba(255, 255, 255, 0.5);
--theme-shadow-md: 0 4px 6px rgba(255, 255, 255, 0.5);
--theme-shadow-lg: 0 10px 15px rgba(255, 255, 255, 0.5);
}
高级主题切换
/* 品牌主题 */
:root[data-theme="brand-blue"] {
--color-primary: #0066cc;
--color-primary-hover: #0052a3;
--color-primary-active: #003d7a;
--color-primary-light: #e6f2ff;
--color-primary-dark: #001a33;
}
:root[data-theme="brand-green"] {
--color-primary: #00cc66;
--color-primary-hover: #00a352;
--color-primary-active: #007a3d;
--color-primary-light: #e6ffe6;
--color-primary-dark: #00331a;
}
:root[data-theme="brand-purple"] {
--color-primary: #6600cc;
--color-primary-hover: #5200a3;
--color-primary-active: #3d007a;
--color-primary-light: #f2e6ff;
--color-primary-dark: #1a0033;
}
/* 动态主题变量 */
:root {
--theme-hue: 220;
--theme-saturation: 70%;
--theme-lightness: 50%;
--color-primary: hsl(var(--theme-hue), var(--theme-saturation), var(--theme-lightness));
--color-primary-hover: hsl(var(--theme-hue), var(--theme-saturation), calc(var(--theme-lightness) - 10%));
--color-primary-active: hsl(var(--theme-hue), var(--theme-saturation), calc(var(--theme-lightness) - 20%));
--color-primary-light: hsl(var(--theme-hue), var(--theme-saturation), 95%);
--color-primary-dark: hsl(var(--theme-hue), var(--theme-saturation), 20%);
}
JavaScript 主题控制
// 主题管理类
class ThemeManager {
private currentTheme: string = 'light'
private themes: Record<string, Record<string, string>> = {}
constructor() {
this.loadThemes()
this.init()
}
private loadThemes() {
this.themes = {
light: {
'--theme-bg-primary': '#ffffff',
'--theme-bg-secondary': '#f8f9fa',
'--theme-text-primary': '#212529',
'--theme-text-secondary': '#6c757d'
},
dark: {
'--theme-bg-primary': '#0d1117',
'--theme-bg-secondary': '#161b22',
'--theme-text-primary': '#f0f6fc',
'--theme-text-secondary': '#8b949e'
},
'brand-blue': {
'--color-primary': '#0066cc',
'--color-primary-hover': '#0052a3',
'--color-primary-light': '#e6f2ff'
}
}
}
private init() {
// 从本地存储加载主题
const savedTheme = localStorage.getItem('theme')
if (savedTheme && this.themes[savedTheme]) {
this.setTheme(savedTheme)
} else {
// 检测系统主题偏好
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
this.setTheme(prefersDark ? 'dark' : 'light')
}
// 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
this.setTheme(e.matches ? 'dark' : 'light')
}
})
}
setTheme(themeName: string) {
if (!this.themes[themeName]) {
console.warn(`Theme "${themeName}" not found`)
return
}
this.currentTheme = themeName
// 应用主题变量
const root = document.documentElement
const theme = this.themes[themeName]
Object.entries(theme).forEach(([property, value]) => {
root.style.setProperty(property, value)
})
// 设置主题属性
root.setAttribute('data-theme', themeName)
// 保存到本地存储
localStorage.setItem('theme', themeName)
// 触发主题变化事件
window.dispatchEvent(new CustomEvent('themechange', {
detail: { theme: themeName }
}))
}
getCurrentTheme(): string {
return this.currentTheme
}
getAvailableThemes(): string[] {
return Object.keys(this.themes)
}
// 动态创建主题
createTheme(name: string, variables: Record<string, string>) {
this.themes[name] = variables
}
// 动态更新主题
updateTheme(themeName: string, variables: Record<string, string>) {
if (this.themes[themeName]) {
this.themes[themeName] = { ...this.themes[themeName], ...variables }
if (this.currentTheme === themeName) {
this.setTheme(themeName)
}
}
}
}
// 使用示例
const themeManager = new ThemeManager()
// 切换主题
themeManager.setTheme('dark')
// 创建自定义主题
themeManager.createTheme('custom', {
'--color-primary': '#ff6b6b',
'--color-secondary': '#4ecdc4',
'--theme-bg-primary': '#f8f9fa'
})
// 动态更新主题
themeManager.updateTheme('custom', {
'--color-primary': '#ff8e8e'
})
🚀 高级变量技巧
条件变量
/* 使用媒体查询创建条件变量 */
:root {
--container-width: 100%;
--container-padding: var(--spacing-md);
}
@media (min-width: 768px) {
:root {
--container-width: 750px;
--container-padding: var(--spacing-lg);
}
}
@media (min-width: 1024px) {
:root {
--container-width: 970px;
--container-padding: var(--spacing-xl);
}
}
.container {
width: var(--container-width);
padding: var(--container-padding);
margin: 0 auto;
}
/* 使用 :has() 选择器创建条件变量 */
.card {
--card-shadow: var(--shadow-sm);
--card-border: 1px solid var(--theme-border-primary);
}
.card:has(.card__image:hover) {
--card-shadow: var(--shadow-lg);
--card-border: 1px solid var(--color-primary);
}
.card:has(.card__button:focus) {
--card-shadow: var(--shadow-md);
--card-border: 2px solid var(--color-primary);
}
动态计算变量
/* 使用 calc() 进行动态计算 */
:root {
--base-size: 16px;
--scale-factor: 1.2;
/* 动态字体大小 */
--font-size-sm: calc(var(--base-size) * 0.875);
--font-size-base: var(--base-size);
--font-size-lg: calc(var(--base-size) * 1.125);
--font-size-xl: calc(var(--base-size) * 1.25);
/* 动态间距 */
--spacing-unit: calc(var(--base-size) * 0.5);
--spacing-sm: var(--spacing-unit);
--spacing-md: calc(var(--spacing-unit) * 2);
--spacing-lg: calc(var(--spacing-unit) * 3);
--spacing-xl: calc(var(--spacing-unit) * 4);
/* 动态圆角 */
--radius-unit: calc(var(--base-size) * 0.25);
--radius-sm: var(--radius-unit);
--radius-md: calc(var(--radius-unit) * 1.5);
--radius-lg: calc(var(--radius-unit) * 2);
}
/* 响应式动态计算 */
:root {
--viewport-width: 100vw;
--viewport-height: 100vh;
/* 基于视口的动态值 */
--dynamic-padding: calc(var(--viewport-width) * 0.05);
--dynamic-margin: calc(var(--viewport-height) * 0.02);
--dynamic-font-size: clamp(14px, calc(var(--viewport-width) * 0.02), 20px);
}
变量链式引用
/* 变量链式引用 */
:root {
--color-primary: #42b883;
--color-primary-rgb: 66, 184, 131;
--color-primary-hsl: 150, 50%, 50%;
/* 基于主色的衍生色 */
--color-primary-light: color-mix(in srgb, var(--color-primary) 20%, white);
--color-primary-dark: color-mix(in srgb, var(--color-primary) 20%, black);
--color-primary-alpha: rgba(var(--color-primary-rgb), 0.1);
--color-primary-alpha-strong: rgba(var(--color-primary-rgb), 0.2);
/* 基于主色的渐变 */
--gradient-primary: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
--gradient-primary-light: linear-gradient(135deg, var(--color-primary-light), var(--color-primary));
/* 基于主色的阴影 */
--shadow-primary: 0 4px 6px rgba(var(--color-primary-rgb), 0.1);
--shadow-primary-lg: 0 10px 15px rgba(var(--color-primary-rgb), 0.2);
}
变量动画
/* 使用 @property 定义可动画的变量 */
@property --hue {
syntax: '<number>';
inherits: false;
initial-value: 220;
}
@property --saturation {
syntax: '<percentage>';
inherits: false;
initial-value: 70%;
}
@property --lightness {
syntax: '<percentage>';
inherits: false;
initial-value: 50%;
}
/* 可动画的颜色变量 */
.animated-color {
--hue: 220;
--saturation: 70%;
--lightness: 50%;
background-color: hsl(var(--hue), var(--saturation), var(--lightness));
transition: --hue 0.5s ease, --saturation 0.5s ease, --lightness 0.5s ease;
}
.animated-color:hover {
--hue: 280;
--saturation: 80%;
--lightness: 60%;
}
/* 可动画的数值变量 */
@property --scale {
syntax: '<number>';
inherits: false;
initial-value: 1;
}
@property --rotation {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.animated-transform {
--scale: 1;
--rotation: 0deg;
transform: scale(var(--scale)) rotate(var(--rotation));
transition: --scale 0.3s ease, --rotation 0.3s ease;
}
.animated-transform:hover {
--scale: 1.1;
--rotation: 5deg;
}
🏗 工程化实践
模块化变量管理
/* tokens/colors.css */
:root {
/* 基础颜色 */
--color-white: #ffffff;
--color-black: #000000;
--color-gray-50: #f9fafb;
--color-gray-100: #f3f4f6;
--color-gray-200: #e5e7eb;
--color-gray-300: #d1d5db;
--color-gray-400: #9ca3af;
--color-gray-500: #6b7280;
--color-gray-600: #4b5563;
--color-gray-700: #374151;
--color-gray-800: #1f2937;
--color-gray-900: #111827;
/* 品牌颜色 */
--color-blue-50: #eff6ff;
--color-blue-500: #3b82f6;
--color-blue-900: #1e3a8a;
--color-green-50: #ecfdf5;
--color-green-500: #10b981;
--color-green-900: #064e3b;
--color-red-50: #fef2f2;
--color-red-500: #ef4444;
--color-red-900: #7f1d1d;
}
/* tokens/spacing.css */
:root {
--space-0: 0;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
--space-16: 4rem;
--space-20: 5rem;
--space-24: 6rem;
--space-32: 8rem;
}
/* tokens/typography.css */
:root {
--font-family-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
--font-family-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-family-mono: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--font-size-2xl: 1.5rem;
--font-size-3xl: 1.875rem;
--font-size-4xl: 2.25rem;
--font-size-5xl: 3rem;
--font-size-6xl: 3.75rem;
--font-weight-thin: 100;
--font-weight-extralight: 200;
--font-weight-light: 300;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-extrabold: 800;
--font-weight-black: 900;
--line-height-none: 1;
--line-height-tight: 1.25;
--line-height-snug: 1.375;
--line-height-normal: 1.5;
--line-height-relaxed: 1.625;
--line-height-loose: 2;
}
/* tokens/shadows.css */
:root {
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
--shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
}
/* tokens/radius.css */
:root {
--radius-none: 0;
--radius-sm: 0.125rem;
--radius: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-2xl: 1rem;
--radius-3xl: 1.5rem;
--radius-full: 9999px;
}
语义化变量系统
/* semantic/colors.css */
:root {
/* 语义颜色 */
--color-primary: var(--color-blue-500);
--color-primary-hover: var(--color-blue-600);
--color-primary-active: var(--color-blue-700);
--color-primary-light: var(--color-blue-50);
--color-primary-dark: var(--color-blue-900);
--color-secondary: var(--color-gray-500);
--color-secondary-hover: var(--color-gray-600);
--color-secondary-active: var(--color-gray-700);
--color-secondary-light: var(--color-gray-50);
--color-secondary-dark: var(--color-gray-900);
--color-success: var(--color-green-500);
--color-success-hover: var(--color-green-600);
--color-success-active: var(--color-green-700);
--color-success-light: var(--color-green-50);
--color-success-dark: var(--color-green-900);
--color-error: var(--color-red-500);
--color-error-hover: var(--color-red-600);
--color-error-active: var(--color-red-700);
--color-error-light: var(--color-red-50);
--color-error-dark: var(--color-red-900);
/* 背景颜色 */
--color-bg-primary: var(--color-white);
--color-bg-secondary: var(--color-gray-50);
--color-bg-tertiary: var(--color-gray-100);
--color-bg-quaternary: var(--color-gray-200);
/* 文本颜色 */
--color-text-primary: var(--color-gray-900);
--color-text-secondary: var(--color-gray-600);
--color-text-tertiary: var(--color-gray-500);
--color-text-quaternary: var(--color-gray-400);
/* 边框颜色 */
--color-border-primary: var(--color-gray-200);
--color-border-secondary: var(--color-gray-300);
--color-border-tertiary: var(--color-gray-400);
}
/* semantic/spacing.css */
:root {
/* 语义间距 */
--spacing-xs: var(--space-1);
--spacing-sm: var(--space-2);
--spacing-md: var(--space-4);
--spacing-lg: var(--space-6);
--spacing-xl: var(--space-8);
--spacing-2xl: var(--space-12);
--spacing-3xl: var(--space-16);
/* 组件间距 */
--spacing-component-xs: var(--spacing-xs);
--spacing-component-sm: var(--spacing-sm);
--spacing-component-md: var(--spacing-md);
--spacing-component-lg: var(--spacing-lg);
--spacing-component-xl: var(--spacing-xl);
/* 布局间距 */
--spacing-layout-xs: var(--spacing-sm);
--spacing-layout-sm: var(--spacing-md);
--spacing-layout-md: var(--spacing-lg);
--spacing-layout-lg: var(--spacing-xl);
--spacing-layout-xl: var(--spacing-2xl);
}
构建工具集成
// postcss.config.js
module.exports = {
plugins: [
require('postcss-custom-properties')({
preserve: false, // 不保留原始变量
importFrom: [
'./src/tokens/colors.css',
'./src/tokens/spacing.css',
'./src/tokens/typography.css'
]
}),
require('postcss-calc')({
precision: 5
}),
require('autoprefixer')
]
}
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
css: {
preprocessorOptions: {
css: {
additionalData: `
@import './src/tokens/colors.css';
@import './src/tokens/spacing.css';
@import './src/tokens/typography.css';
`
}
}
}
})
🎨 实战项目
项目1:动态主题系统
<!-- ThemeProvider.vue -->
<template>
<div class="theme-provider" :data-theme="currentTheme">
<slot />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, provide } from 'vue'
interface Theme {
name: string
colors: Record<string, string>
spacing: Record<string, string>
typography: Record<string, string>
}
const currentTheme = ref('light')
const themes: Record<string, Theme> = {
light: {
name: 'light',
colors: {
'--color-bg-primary': '#ffffff',
'--color-bg-secondary': '#f8f9fa',
'--color-text-primary': '#212529',
'--color-text-secondary': '#6c757d',
'--color-primary': '#42b883',
'--color-primary-hover': '#369870'
},
spacing: {
'--spacing-sm': '0.5rem',
'--spacing-md': '1rem',
'--spacing-lg': '1.5rem'
},
typography: {
'--font-size-sm': '0.875rem',
'--font-size-base': '1rem',
'--font-size-lg': '1.125rem'
}
},
dark: {
name: 'dark',
colors: {
'--color-bg-primary': '#0d1117',
'--color-bg-secondary': '#161b22',
'--color-text-primary': '#f0f6fc',
'--color-text-secondary': '#8b949e',
'--color-primary': '#64b5f6',
'--color-primary-hover': '#42a5f5'
},
spacing: {
'--spacing-sm': '0.5rem',
'--spacing-md': '1rem',
'--spacing-lg': '1.5rem'
},
typography: {
'--font-size-sm': '0.875rem',
'--font-size-base': '1rem',
'--font-size-lg': '1.125rem'
}
}
}
const setTheme = (themeName: string) => {
if (!themes[themeName]) return
currentTheme.value = themeName
const theme = themes[themeName]
// 应用主题变量
const root = document.documentElement
Object.entries(theme.colors).forEach(([property, value]) => {
root.style.setProperty(property, value)
})
Object.entries(theme.spacing).forEach(([property, value]) => {
root.style.setProperty(property, value)
})
Object.entries(theme.typography).forEach(([property, value]) => {
root.style.setProperty(property, value)
})
// 保存到本地存储
localStorage.setItem('theme', themeName)
}
const toggleTheme = () => {
const newTheme = currentTheme.value === 'light' ? 'dark' : 'light'
setTheme(newTheme)
}
// 提供主题控制方法
provide('theme', {
currentTheme,
setTheme,
toggleTheme,
availableThemes: Object.keys(themes)
})
onMounted(() => {
const savedTheme = localStorage.getItem('theme')
if (savedTheme && themes[savedTheme]) {
setTheme(savedTheme)
}
})
</script>
<style scoped>
.theme-provider {
min-height: 100vh;
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
transition: background-color 0.3s ease, color 0.3s ease;
}
</style>
项目2:响应式设计系统
<!-- DesignSystem.vue -->
<template>
<div class="design-system">
<header class="design-system__header">
<h1>设计系统</h1>
<ThemeToggle />
</header>
<main class="design-system__content">
<section class="design-system__section">
<h2>颜色系统</h2>
<div class="color-palette">
<div
v-for="color in colorTokens"
:key="color.name"
class="color-swatch"
:style="{ backgroundColor: color.value }"
>
<span class="color-swatch__name">{{ color.name }}</span>
<span class="color-swatch__value">{{ color.value }}</span>
</div>
</div>
</section>
<section class="design-system__section">
<h2>间距系统</h2>
<div class="spacing-demo">
<div
v-for="space in spacingTokens"
:key="space.name"
class="spacing-item"
>
<div
class="spacing-item__visual"
:style="{ width: space.value, height: space.value }"
></div>
<span class="spacing-item__name">{{ space.name }}</span>
<span class="spacing-item__value">{{ space.value }}</span>
</div>
</div>
</section>
<section class="design-system__section">
<h2>组件示例</h2>
<div class="component-showcase">
<Button variant="primary" size="sm">小按钮</Button>
<Button variant="primary" size="md">中按钮</Button>
<Button variant="primary" size="lg">大按钮</Button>
<Button variant="secondary" size="md">次要按钮</Button>
<Button variant="outline" size="md">轮廓按钮</Button>
</div>
</section>
</main>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import ThemeToggle from './ThemeToggle.vue'
import Button from './Button.vue'
const colorTokens = ref([
{ name: 'Primary', value: 'var(--color-primary)' },
{ name: 'Secondary', value: 'var(--color-secondary)' },
{ name: 'Success', value: 'var(--color-success)' },
{ name: 'Warning', value: 'var(--color-warning)' },
{ name: 'Error', value: 'var(--color-error)' }
])
const spacingTokens = ref([
{ name: 'XS', value: 'var(--spacing-xs)' },
{ name: 'SM', value: 'var(--spacing-sm)' },
{ name: 'MD', value: 'var(--spacing-md)' },
{ name: 'LG', value: 'var(--spacing-lg)' },
{ name: 'XL', value: 'var(--spacing-xl)' }
])
</script>
<style scoped>
.design-system {
min-height: 100vh;
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
}
.design-system__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-lg);
border-bottom: 1px solid var(--color-border-primary);
}
.design-system__content {
padding: var(--spacing-lg);
max-width: 1200px;
margin: 0 auto;
}
.design-system__section {
margin-bottom: var(--spacing-2xl);
}
.color-palette {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-md);
}
.color-swatch {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: var(--spacing-md);
border-radius: var(--radius-md);
min-height: 100px;
color: white;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.color-swatch__name {
font-weight: var(--font-weight-semibold);
}
.color-swatch__value {
font-size: var(--font-size-sm);
opacity: 0.8;
}
.spacing-demo {
display: flex;
gap: var(--spacing-md);
align-items: end;
}
.spacing-item {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-sm);
}
.spacing-item__visual {
background-color: var(--color-primary);
border-radius: var(--radius-sm);
}
.spacing-item__name {
font-weight: var(--font-weight-medium);
}
.spacing-item__value {
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
}
.component-showcase {
display: flex;
gap: var(--spacing-md);
align-items: center;
flex-wrap: wrap;
}
</style>
📚 最佳实践总结
1. 变量命名规范
/* ✅ 好的命名 */
:root {
--color-primary: #42b883;
--spacing-md: 1rem;
--font-size-lg: 1.125rem;
--btn-padding-x: 1rem;
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* ❌ 避免的命名 */
:root {
--blue: #42b883;
--big: 1rem;
--large: 1.125rem;
--button-padding: 1rem;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
2. 变量组织原则
/* ✅ 按功能分组 */
:root {
/* 颜色变量 */
--color-primary: #42b883;
--color-secondary: #35495e;
/* 间距变量 */
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
/* 字体变量 */
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
}
3. 性能优化
/* ✅ 使用 CSS 变量而不是 JavaScript 计算 */
.component {
--component-width: calc(100% - var(--spacing-md) * 2);
width: var(--component-width);
}
/* ✅ 合理使用 will-change */
.animated-component {
will-change: transform, opacity;
transition: transform 0.3s ease, opacity 0.3s ease;
}
4. 兼容性考虑
/* ✅ 提供回退值 */
.component {
background: var(--color-primary, #42b883);
font-size: var(--font-size-base, 16px);
}
/* ✅ 使用 @supports 检测支持 */
@supports (color: color-mix(in srgb, red, blue)) {
.component {
--color-primary-light: color-mix(in srgb, var(--color-primary) 20%, white);
}
}
🚨 常见问题与解决方案
1. 变量不生效
问题:CSS 变量没有应用 解决:检查变量名拼写和作用域
/* ❌ 错误 */
.component {
color: var(--primary-color); /* 变量名错误 */
}
/* ✅ 正确 */
.component {
color: var(--color-primary); /* 正确的变量名 */
}
2. 变量继承问题
问题:子元素没有继承父元素的变量 解决:确保变量在正确的作用域中定义
/* ❌ 错误 */
.parent {
--local-var: red;
}
.child {
color: var(--local-var); /* 可能不生效 */
}
/* ✅ 正确 */
:root {
--global-var: red;
}
.child {
color: var(--global-var); /* 会生效 */
}
3. 性能问题
问题:大量变量导致性能问题 解决:合理组织变量,避免过度使用
/* ❌ 过度使用 */
.component {
--var1: value1;
--var2: value2;
--var3: value3;
/* ... 太多变量 */
}
/* ✅ 合理使用 */
.component {
--component-bg: var(--color-bg-secondary);
--component-padding: var(--spacing-md);
--component-radius: var(--radius-md);
}
📖 延伸阅读
通过本章的学习,你已经掌握了 CSS 变量体系与主题系统的核心知识。这些技能不仅能提升开发效率,更能为构建现代化、可维护的用户界面提供强大支持。继续实践和探索,你将能够设计出更加优雅和灵活的设计系统。