HiHuo
首页
博客
手册
工具
首页
博客
手册
工具
  • Vue & CSS 进阶

    • 第0章:工具链与开发体验
    • 第1章:TypeScript 核心
    • 第2章:CSS 现代能力
    • 第3章:Vue3 核心与原理
    • 第4章:路由与状态管理
    • 第5章:工程化与性能
    • 第6章:测试与质量
    • 第7章:CSS 进阶专题
    • 第8章:项目实战A - SaaS仪表盘
    • 第9章:项目实战B - 内容社区
    • 第10章:Vue 内核深入
    • 第11章:微前端与部署
    • CSS 变量体系与主题系统深度指南
    • 前端安全与防护专题
    • CSS 专题:动效优化与微交互
    • CSS 专题:图片与图形处理
    • 实时通信与WebSocket专题
    • 开发工具与调试技巧专题
    • 新兴技术与未来趋势专题
    • 移动端开发与PWA专题
    • 附录A:代码片段库
    • 附录B:练习题集

CSS 变量体系与主题系统深度指南

深入探索 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 自定义属性规范
  • CSS 变量最佳实践
  • 设计令牌规范
  • CSS 变量动画

通过本章的学习,你已经掌握了 CSS 变量体系与主题系统的核心知识。这些技能不仅能提升开发效率,更能为构建现代化、可维护的用户界面提供强大支持。继续实践和探索,你将能够设计出更加优雅和灵活的设计系统。

Prev
第11章:微前端与部署
Next
前端安全与防护专题