Skip to content

Advanced Theme Customization and Design System

Overview

This document explores Element Plus's advanced theme customization capabilities and design system architecture. We'll examine CSS custom properties, design tokens, theme switching, and building comprehensive design systems.

Design System Architecture

1. Design Token Structure

Element Plus uses a hierarchical design token system for consistent theming.

scss
// Core design tokens
:root {
  // Color palette
  --el-color-primary: #409eff;
  --el-color-primary-light-3: #79bbff;
  --el-color-primary-light-5: #a0cfff;
  --el-color-primary-light-7: #c6e2ff;
  --el-color-primary-light-8: #d9ecff;
  --el-color-primary-light-9: #ecf5ff;
  --el-color-primary-dark-2: #337ecc;
  
  // Semantic colors
  --el-color-success: #67c23a;
  --el-color-warning: #e6a23c;
  --el-color-danger: #f56c6c;
  --el-color-error: #f56c6c;
  --el-color-info: #909399;
  
  // Text colors
  --el-text-color-primary: #303133;
  --el-text-color-regular: #606266;
  --el-text-color-secondary: #909399;
  --el-text-color-placeholder: #a8abb2;
  --el-text-color-disabled: #c0c4cc;
  
  // Border colors
  --el-border-color: #dcdfe6;
  --el-border-color-light: #e4e7ed;
  --el-border-color-lighter: #ebeef5;
  --el-border-color-extra-light: #f2f6fc;
  --el-border-color-dark: #d4d7de;
  --el-border-color-darker: #cdd0d6;
  
  // Background colors
  --el-bg-color: #ffffff;
  --el-bg-color-page: #f2f3f5;
  --el-bg-color-overlay: #ffffff;
  
  // Typography
  --el-font-size-extra-large: 20px;
  --el-font-size-large: 18px;
  --el-font-size-medium: 16px;
  --el-font-size-base: 14px;
  --el-font-size-small: 13px;
  --el-font-size-extra-small: 12px;
  
  --el-font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
  --el-font-weight-primary: 500;
  
  // Spacing
  --el-spacing-xs: 4px;
  --el-spacing-sm: 8px;
  --el-spacing-md: 12px;
  --el-spacing-lg: 16px;
  --el-spacing-xl: 20px;
  --el-spacing-xxl: 24px;
  
  // Border radius
  --el-border-radius-base: 4px;
  --el-border-radius-small: 2px;
  --el-border-radius-round: 20px;
  --el-border-radius-circle: 100%;
  
  // Shadows
  --el-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
  --el-box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  --el-box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
  --el-box-shadow-dark: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.12);
  
  // Z-index
  --el-index-normal: 1;
  --el-index-top: 1000;
  --el-index-popper: 2000;
  
  // Transitions
  --el-transition-duration: 0.3s;
  --el-transition-duration-fast: 0.2s;
  --el-transition-function-ease-in-out-bezier: cubic-bezier(0.645, 0.045, 0.355, 1);
  --el-transition-function-fast-bezier: cubic-bezier(0.23, 1, 0.32, 1);
  --el-transition-all: all var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier);
  --el-transition-fade: opacity var(--el-transition-duration) var(--el-transition-function-fast-bezier);
  --el-transition-md-fade: transform var(--el-transition-duration) var(--el-transition-function-fast-bezier), opacity var(--el-transition-duration) var(--el-transition-function-fast-bezier);
  --el-transition-fade-linear: opacity var(--el-transition-duration-fast) linear;
  --el-transition-border: border-color var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);
  --el-transition-box-shadow: box-shadow var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);
  --el-transition-color: color var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);
}

2. Component-Specific Tokens

scss
// Button component tokens
:root {
  --el-button-font-weight: var(--el-font-weight-primary);
  --el-button-border-color: var(--el-border-color);
  --el-button-bg-color: var(--el-bg-color);
  --el-button-text-color: var(--el-text-color-regular);
  --el-button-disabled-text-color: var(--el-text-color-disabled);
  --el-button-disabled-bg-color: var(--el-bg-color);
  --el-button-disabled-border-color: var(--el-border-color-light);
  --el-button-divide-border-color: rgba(255, 255, 255, 0.5);
  --el-button-hover-text-color: var(--el-color-primary);
  --el-button-hover-bg-color: var(--el-color-primary-light-9);
  --el-button-hover-border-color: var(--el-color-primary-light-7);
  --el-button-active-text-color: var(--el-color-primary-dark-2);
  --el-button-active-border-color: var(--el-color-primary-dark-2);
  --el-button-active-bg-color: var(--el-color-primary-light-9);
  --el-button-outline-color: var(--el-color-primary-light-5);
  --el-button-active-color: var(--el-text-color-primary);
}

// Form component tokens
:root {
  --el-form-label-font-size: var(--el-font-size-base);
  --el-form-inline-content-width: 220px;
}

// Input component tokens
:root {
  --el-input-text-color: var(--el-text-color-regular);
  --el-input-border: var(--el-border);
  --el-input-hover-border: var(--el-border-color-hover);
  --el-input-focus-border: var(--el-color-primary);
  --el-input-transparent-border: 0 0 0 1px transparent inset;
  --el-input-border-color: var(--el-border-color);
  --el-input-border-radius: var(--el-border-radius-base);
  --el-input-bg-color: var(--el-bg-color);
  --el-input-icon-color: var(--el-text-color-placeholder);
  --el-input-placeholder-color: var(--el-text-color-placeholder);
  --el-input-hover-border-color: var(--el-border-color-hover);
  --el-input-clear-hover-color: var(--el-text-color-secondary);
  --el-input-focus-border-color: var(--el-color-primary);
}

Advanced Theme System

1. Theme Manager Implementation

typescript
// Advanced theme management system
import { reactive, computed, watch } from 'vue'

interface ThemeConfig {
  name: string
  colors: {
    primary: string
    success: string
    warning: string
    danger: string
    info: string
  }
  typography: {
    fontFamily: string
    fontSize: {
      base: string
      small: string
      large: string
    }
    fontWeight: {
      normal: number
      medium: number
      bold: number
    }
  }
  spacing: {
    xs: string
    sm: string
    md: string
    lg: string
    xl: string
  }
  borderRadius: {
    small: string
    base: string
    large: string
  }
  shadows: {
    base: string
    light: string
    dark: string
  }
  transitions: {
    duration: string
    easing: string
  }
}

class AdvancedThemeManager {
  private themes = new Map<string, ThemeConfig>()
  private state = reactive({
    currentTheme: 'default',
    customizations: {} as Record<string, any>,
    darkMode: false,
    highContrast: false,
    reducedMotion: false
  })
  
  constructor() {
    this.initializeDefaultThemes()
    this.setupMediaQueryListeners()
  }
  
  private initializeDefaultThemes() {
    // Default light theme
    this.registerTheme('default', {
      name: 'Default',
      colors: {
        primary: '#409eff',
        success: '#67c23a',
        warning: '#e6a23c',
        danger: '#f56c6c',
        info: '#909399'
      },
      typography: {
        fontFamily: '"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif',
        fontSize: {
          base: '14px',
          small: '12px',
          large: '16px'
        },
        fontWeight: {
          normal: 400,
          medium: 500,
          bold: 600
        }
      },
      spacing: {
        xs: '4px',
        sm: '8px',
        md: '12px',
        lg: '16px',
        xl: '20px'
      },
      borderRadius: {
        small: '2px',
        base: '4px',
        large: '8px'
      },
      shadows: {
        base: '0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04)',
        light: '0 2px 12px 0 rgba(0, 0, 0, 0.1)',
        dark: '0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.12)'
      },
      transitions: {
        duration: '0.3s',
        easing: 'cubic-bezier(0.645, 0.045, 0.355, 1)'
      }
    })
    
    // Dark theme
    this.registerTheme('dark', {
      name: 'Dark',
      colors: {
        primary: '#409eff',
        success: '#67c23a',
        warning: '#e6a23c',
        danger: '#f56c6c',
        info: '#909399'
      },
      typography: {
        fontFamily: '"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif',
        fontSize: {
          base: '14px',
          small: '12px',
          large: '16px'
        },
        fontWeight: {
          normal: 400,
          medium: 500,
          bold: 600
        }
      },
      spacing: {
        xs: '4px',
        sm: '8px',
        md: '12px',
        lg: '16px',
        xl: '20px'
      },
      borderRadius: {
        small: '2px',
        base: '4px',
        large: '8px'
      },
      shadows: {
        base: '0 2px 4px rgba(0, 0, 0, 0.24), 0 0 6px rgba(0, 0, 0, 0.08)',
        light: '0 2px 12px 0 rgba(0, 0, 0, 0.2)',
        dark: '0 2px 4px rgba(0, 0, 0, 0.24), 0 0 6px rgba(0, 0, 0, 0.24)'
      },
      transitions: {
        duration: '0.3s',
        easing: 'cubic-bezier(0.645, 0.045, 0.355, 1)'
      }
    })
  }
  
  private setupMediaQueryListeners() {
    // Dark mode detection
    const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)')
    darkModeQuery.addEventListener('change', (e) => {
      this.state.darkMode = e.matches
    })
    this.state.darkMode = darkModeQuery.matches
    
    // High contrast detection
    const highContrastQuery = window.matchMedia('(prefers-contrast: high)')
    highContrastQuery.addEventListener('change', (e) => {
      this.state.highContrast = e.matches
    })
    this.state.highContrast = highContrastQuery.matches
    
    // Reduced motion detection
    const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
    reducedMotionQuery.addEventListener('change', (e) => {
      this.state.reducedMotion = e.matches
    })
    this.state.reducedMotion = reducedMotionQuery.matches
  }
  
  registerTheme(name: string, config: ThemeConfig) {
    this.themes.set(name, config)
  }
  
  setTheme(name: string) {
    const theme = this.themes.get(name)
    if (!theme) {
      console.warn(`Theme ${name} not found`)
      return
    }
    
    this.state.currentTheme = name
    this.applyTheme(theme)
  }
  
  private applyTheme(theme: ThemeConfig) {
    const root = document.documentElement
    
    // Apply color tokens
    Object.entries(theme.colors).forEach(([key, value]) => {
      root.style.setProperty(`--el-color-${key}`, value)
      
      // Generate color variations
      if (key === 'primary') {
        this.generateColorVariations(root, value)
      }
    })
    
    // Apply typography tokens
    root.style.setProperty('--el-font-family', theme.typography.fontFamily)
    Object.entries(theme.typography.fontSize).forEach(([key, value]) => {
      root.style.setProperty(`--el-font-size-${key}`, value)
    })
    Object.entries(theme.typography.fontWeight).forEach(([key, value]) => {
      root.style.setProperty(`--el-font-weight-${key}`, value.toString())
    })
    
    // Apply spacing tokens
    Object.entries(theme.spacing).forEach(([key, value]) => {
      root.style.setProperty(`--el-spacing-${key}`, value)
    })
    
    // Apply border radius tokens
    Object.entries(theme.borderRadius).forEach(([key, value]) => {
      root.style.setProperty(`--el-border-radius-${key}`, value)
    })
    
    // Apply shadow tokens
    Object.entries(theme.shadows).forEach(([key, value]) => {
      root.style.setProperty(`--el-box-shadow-${key}`, value)
    })
    
    // Apply transition tokens
    root.style.setProperty('--el-transition-duration', theme.transitions.duration)
    root.style.setProperty('--el-transition-function-ease-in-out-bezier', theme.transitions.easing)
    
    // Apply accessibility preferences
    this.applyAccessibilityPreferences()
  }
  
  private generateColorVariations(root: HTMLElement, primaryColor: string) {
    // Generate light variations
    for (let i = 1; i <= 9; i++) {
      const lightColor = this.lightenColor(primaryColor, i * 10)
      root.style.setProperty(`--el-color-primary-light-${i}`, lightColor)
    }
    
    // Generate dark variations
    for (let i = 1; i <= 3; i++) {
      const darkColor = this.darkenColor(primaryColor, i * 10)
      root.style.setProperty(`--el-color-primary-dark-${i}`, darkColor)
    }
  }
  
  private lightenColor(color: string, percent: number): string {
    // Convert hex to RGB
    const hex = color.replace('#', '')
    const r = parseInt(hex.substr(0, 2), 16)
    const g = parseInt(hex.substr(2, 2), 16)
    const b = parseInt(hex.substr(4, 2), 16)
    
    // Lighten
    const newR = Math.min(255, Math.round(r + (255 - r) * percent / 100))
    const newG = Math.min(255, Math.round(g + (255 - g) * percent / 100))
    const newB = Math.min(255, Math.round(b + (255 - b) * percent / 100))
    
    // Convert back to hex
    return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`
  }
  
  private darkenColor(color: string, percent: number): string {
    // Convert hex to RGB
    const hex = color.replace('#', '')
    const r = parseInt(hex.substr(0, 2), 16)
    const g = parseInt(hex.substr(2, 2), 16)
    const b = parseInt(hex.substr(4, 2), 16)
    
    // Darken
    const newR = Math.max(0, Math.round(r * (100 - percent) / 100))
    const newG = Math.max(0, Math.round(g * (100 - percent) / 100))
    const newB = Math.max(0, Math.round(b * (100 - percent) / 100))
    
    // Convert back to hex
    return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`
  }
  
  private applyAccessibilityPreferences() {
    const root = document.documentElement
    
    if (this.state.highContrast) {
      root.classList.add('el-high-contrast')
    } else {
      root.classList.remove('el-high-contrast')
    }
    
    if (this.state.reducedMotion) {
      root.style.setProperty('--el-transition-duration', '0s')
      root.style.setProperty('--el-transition-duration-fast', '0s')
    }
    
    if (this.state.darkMode && this.state.currentTheme === 'default') {
      this.setTheme('dark')
    }
  }
  
  customizeTheme(customizations: Record<string, any>) {
    this.state.customizations = { ...this.state.customizations, ...customizations }
    this.applyCustomizations()
  }
  
  private applyCustomizations() {
    const root = document.documentElement
    
    Object.entries(this.state.customizations).forEach(([key, value]) => {
      root.style.setProperty(`--el-${key}`, value)
    })
  }
  
  exportTheme(): string {
    const currentTheme = this.themes.get(this.state.currentTheme)
    if (!currentTheme) return ''
    
    return JSON.stringify({
      theme: currentTheme,
      customizations: this.state.customizations
    }, null, 2)
  }
  
  importTheme(themeData: string) {
    try {
      const { theme, customizations } = JSON.parse(themeData)
      
      if (theme) {
        this.registerTheme('imported', theme)
        this.setTheme('imported')
      }
      
      if (customizations) {
        this.customizeTheme(customizations)
      }
    } catch (error) {
      console.error('Failed to import theme:', error)
    }
  }
  
  // Getters
  get currentTheme() {
    return this.state.currentTheme
  }
  
  get availableThemes() {
    return Array.from(this.themes.keys())
  }
  
  get isDarkMode() {
    return this.state.darkMode
  }
  
  get isHighContrast() {
    return this.state.highContrast
  }
  
  get isReducedMotion() {
    return this.state.reducedMotion
  }
}

export const themeManager = new AdvancedThemeManager()

2. Theme Builder Component

vue
<!-- ThemeBuilder.vue -->
<template>
  <div class="theme-builder">
    <el-card class="theme-builder__header">
      <template #header>
        <div class="theme-builder__title">
          <h2>Theme Builder</h2>
          <div class="theme-builder__actions">
            <el-button @click="resetTheme">Reset</el-button>
            <el-button @click="exportTheme">Export</el-button>
            <el-button type="primary" @click="saveTheme">Save</el-button>
          </div>
        </div>
      </template>
      
      <div class="theme-builder__presets">
        <h3>Presets</h3>
        <div class="preset-grid">
          <div
            v-for="theme in availableThemes"
            :key="theme"
            class="preset-item"
            :class="{ active: currentTheme === theme }"
            @click="setTheme(theme)"
          >
            <div class="preset-preview">
              <div class="preset-colors">
                <div
                  v-for="color in getThemeColors(theme)"
                  :key="color.name"
                  class="preset-color"
                  :style="{ backgroundColor: color.value }"
                />
              </div>
            </div>
            <span class="preset-name">{{ theme }}</span>
          </div>
        </div>
      </div>
    </el-card>
    
    <div class="theme-builder__content">
      <div class="theme-builder__sidebar">
        <el-tabs v-model="activeTab" tab-position="left">
          <el-tab-pane label="Colors" name="colors">
            <div class="color-section">
              <h4>Brand Colors</h4>
              <div class="color-group">
                <div
                  v-for="(color, key) in colors.brand"
                  :key="key"
                  class="color-item"
                >
                  <label>{{ formatLabel(key) }}</label>
                  <div class="color-input-group">
                    <el-color-picker
                      v-model="colors.brand[key]"
                      @change="updateColor('brand', key, $event)"
                    />
                    <el-input
                      v-model="colors.brand[key]"
                      size="small"
                      @input="updateColor('brand', key, $event)"
                    />
                  </div>
                </div>
              </div>
              
              <h4>Semantic Colors</h4>
              <div class="color-group">
                <div
                  v-for="(color, key) in colors.semantic"
                  :key="key"
                  class="color-item"
                >
                  <label>{{ formatLabel(key) }}</label>
                  <div class="color-input-group">
                    <el-color-picker
                      v-model="colors.semantic[key]"
                      @change="updateColor('semantic', key, $event)"
                    />
                    <el-input
                      v-model="colors.semantic[key]"
                      size="small"
                      @input="updateColor('semantic', key, $event)"
                    />
                  </div>
                </div>
              </div>
              
              <h4>Text Colors</h4>
              <div class="color-group">
                <div
                  v-for="(color, key) in colors.text"
                  :key="key"
                  class="color-item"
                >
                  <label>{{ formatLabel(key) }}</label>
                  <div class="color-input-group">
                    <el-color-picker
                      v-model="colors.text[key]"
                      @change="updateColor('text', key, $event)"
                    />
                    <el-input
                      v-model="colors.text[key]"
                      size="small"
                      @input="updateColor('text', key, $event)"
                    />
                  </div>
                </div>
              </div>
            </div>
          </el-tab-pane>
          
          <el-tab-pane label="Typography" name="typography">
            <div class="typography-section">
              <h4>Font Family</h4>
              <el-input
                v-model="typography.fontFamily"
                @input="updateTypography('fontFamily', $event)"
              />
              
              <h4>Font Sizes</h4>
              <div class="size-group">
                <div
                  v-for="(size, key) in typography.fontSize"
                  :key="key"
                  class="size-item"
                >
                  <label>{{ formatLabel(key) }}</label>
                  <el-input-number
                    v-model="typography.fontSize[key]"
                    :min="8"
                    :max="48"
                    @change="updateTypography('fontSize', key, $event + 'px')"
                  />
                </div>
              </div>
              
              <h4>Font Weights</h4>
              <div class="weight-group">
                <div
                  v-for="(weight, key) in typography.fontWeight"
                  :key="key"
                  class="weight-item"
                >
                  <label>{{ formatLabel(key) }}</label>
                  <el-select
                    v-model="typography.fontWeight[key]"
                    @change="updateTypography('fontWeight', key, $event)"
                  >
                    <el-option label="Light (300)" :value="300" />
                    <el-option label="Normal (400)" :value="400" />
                    <el-option label="Medium (500)" :value="500" />
                    <el-option label="Semibold (600)" :value="600" />
                    <el-option label="Bold (700)" :value="700" />
                  </el-select>
                </div>
              </div>
            </div>
          </el-tab-pane>
          
          <el-tab-pane label="Spacing" name="spacing">
            <div class="spacing-section">
              <h4>Spacing Scale</h4>
              <div class="spacing-group">
                <div
                  v-for="(space, key) in spacing"
                  :key="key"
                  class="spacing-item"
                >
                  <label>{{ formatLabel(key) }}</label>
                  <div class="spacing-input-group">
                    <el-input-number
                      v-model="spacingValues[key]"
                      :min="0"
                      :max="100"
                      @change="updateSpacing(key, $event + 'px')"
                    />
                    <div class="spacing-preview" :style="{ width: space, height: space }" />
                  </div>
                </div>
              </div>
            </div>
          </el-tab-pane>
          
          <el-tab-pane label="Border Radius" name="borderRadius">
            <div class="border-radius-section">
              <h4>Border Radius Scale</h4>
              <div class="border-radius-group">
                <div
                  v-for="(radius, key) in borderRadius"
                  :key="key"
                  class="border-radius-item"
                >
                  <label>{{ formatLabel(key) }}</label>
                  <div class="border-radius-input-group">
                    <el-input-number
                      v-model="borderRadiusValues[key]"
                      :min="0"
                      :max="50"
                      @change="updateBorderRadius(key, $event + 'px')"
                    />
                    <div
                      class="border-radius-preview"
                      :style="{ borderRadius: radius }"
                    />
                  </div>
                </div>
              </div>
            </div>
          </el-tab-pane>
          
          <el-tab-pane label="Shadows" name="shadows">
            <div class="shadows-section">
              <h4>Shadow Presets</h4>
              <div class="shadow-group">
                <div
                  v-for="(shadow, key) in shadows"
                  :key="key"
                  class="shadow-item"
                >
                  <label>{{ formatLabel(key) }}</label>
                  <div class="shadow-input-group">
                    <el-input
                      v-model="shadows[key]"
                      @input="updateShadow(key, $event)"
                    />
                    <div
                      class="shadow-preview"
                      :style="{ boxShadow: shadow }"
                    />
                  </div>
                </div>
              </div>
            </div>
          </el-tab-pane>
        </el-tabs>
      </div>
      
      <div class="theme-builder__preview">
        <h3>Preview</h3>
        <div class="preview-content">
          <ComponentPreview />
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { themeManager } from './theme-manager'
import ComponentPreview from './ComponentPreview.vue'

const activeTab = ref('colors')
const currentTheme = ref(themeManager.currentTheme)

const colors = reactive({
  brand: {
    primary: '#409eff'
  },
  semantic: {
    success: '#67c23a',
    warning: '#e6a23c',
    danger: '#f56c6c',
    info: '#909399'
  },
  text: {
    primary: '#303133',
    regular: '#606266',
    secondary: '#909399',
    placeholder: '#a8abb2',
    disabled: '#c0c4cc'
  }
})

const typography = reactive({
  fontFamily: '"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif',
  fontSize: {
    small: 12,
    base: 14,
    large: 16
  },
  fontWeight: {
    normal: 400,
    medium: 500,
    bold: 600
  }
})

const spacing = reactive({
  xs: '4px',
  sm: '8px',
  md: '12px',
  lg: '16px',
  xl: '20px'
})

const spacingValues = reactive({
  xs: 4,
  sm: 8,
  md: 12,
  lg: 16,
  xl: 20
})

const borderRadius = reactive({
  small: '2px',
  base: '4px',
  large: '8px'
})

const borderRadiusValues = reactive({
  small: 2,
  base: 4,
  large: 8
})

const shadows = reactive({
  base: '0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04)',
  light: '0 2px 12px 0 rgba(0, 0, 0, 0.1)',
  dark: '0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.12)'
})

const availableThemes = computed(() => themeManager.availableThemes)

const updateColor = (category: string, key: string, value: string) => {
  const cssVar = `color-${category === 'brand' ? key : `${category}-${key}`}`
  themeManager.customizeTheme({ [cssVar]: value })
}

const updateTypography = (category: string, key: string, value: any) => {
  const cssVar = category === 'fontFamily' ? 'font-family' : `font-${category.replace('font', '').toLowerCase()}-${key}`
  themeManager.customizeTheme({ [cssVar]: value })
}

const updateSpacing = (key: string, value: string) => {
  spacing[key] = value
  themeManager.customizeTheme({ [`spacing-${key}`]: value })
}

const updateBorderRadius = (key: string, value: string) => {
  borderRadius[key] = value
  themeManager.customizeTheme({ [`border-radius-${key}`]: value })
}

const updateShadow = (key: string, value: string) => {
  themeManager.customizeTheme({ [`box-shadow-${key}`]: value })
}

const setTheme = (theme: string) => {
  themeManager.setTheme(theme)
  currentTheme.value = theme
}

const resetTheme = () => {
  themeManager.setTheme('default')
  currentTheme.value = 'default'
}

const exportTheme = () => {
  const themeData = themeManager.exportTheme()
  const blob = new Blob([themeData], { type: 'application/json' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = 'theme.json'
  a.click()
  URL.revokeObjectURL(url)
}

const saveTheme = () => {
  // Implementation for saving theme
  console.log('Theme saved')
}

const formatLabel = (key: string) => {
  return key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())
}

const getThemeColors = (themeName: string) => {
  // Implementation to get theme colors for preview
  return [
    { name: 'primary', value: '#409eff' },
    { name: 'success', value: '#67c23a' },
    { name: 'warning', value: '#e6a23c' },
    { name: 'danger', value: '#f56c6c' }
  ]
}
</script>

<style scoped>
.theme-builder {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.theme-builder__header {
  margin-bottom: 20px;
}

.theme-builder__title {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.theme-builder__title h2 {
  margin: 0;
}

.preset-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 16px;
  margin-top: 16px;
}

.preset-item {
  cursor: pointer;
  border: 2px solid transparent;
  border-radius: 8px;
  padding: 12px;
  text-align: center;
  transition: all 0.3s;
}

.preset-item:hover {
  border-color: var(--el-color-primary-light-7);
}

.preset-item.active {
  border-color: var(--el-color-primary);
}

.preset-preview {
  margin-bottom: 8px;
}

.preset-colors {
  display: flex;
  gap: 4px;
  justify-content: center;
}

.preset-color {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 1px solid var(--el-border-color);
}

.preset-name {
  font-size: 12px;
  color: var(--el-text-color-regular);
  text-transform: capitalize;
}

.theme-builder__content {
  display: grid;
  grid-template-columns: 400px 1fr;
  gap: 20px;
}

.theme-builder__sidebar {
  border: 1px solid var(--el-border-color);
  border-radius: 8px;
  overflow: hidden;
}

.color-section,
.typography-section,
.spacing-section,
.border-radius-section,
.shadows-section {
  padding: 20px;
}

.color-group,
.size-group,
.weight-group,
.spacing-group,
.border-radius-group,
.shadow-group {
  margin-top: 16px;
}

.color-item,
.size-item,
.weight-item,
.spacing-item,
.border-radius-item,
.shadow-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
}

.color-item label,
.size-item label,
.weight-item label,
.spacing-item label,
.border-radius-item label,
.shadow-item label {
  font-weight: 500;
  min-width: 80px;
}

.color-input-group {
  display: flex;
  gap: 8px;
  align-items: center;
}

.color-input-group .el-input {
  width: 80px;
}

.spacing-input-group,
.border-radius-input-group,
.shadow-input-group {
  display: flex;
  gap: 8px;
  align-items: center;
}

.spacing-preview,
.border-radius-preview,
.shadow-preview {
  width: 24px;
  height: 24px;
  background-color: var(--el-color-primary);
  border: 1px solid var(--el-border-color);
}

.border-radius-preview {
  background-color: var(--el-color-primary-light-9);
  border-color: var(--el-color-primary);
}

.shadow-preview {
  background-color: var(--el-bg-color);
}

.theme-builder__preview {
  border: 1px solid var(--el-border-color);
  border-radius: 8px;
  padding: 20px;
}

.theme-builder__preview h3 {
  margin-top: 0;
  margin-bottom: 20px;
}

.preview-content {
  min-height: 600px;
}
</style>

3. CSS Custom Properties Integration

scss
// Advanced CSS custom properties usage
.el-button {
  // Use CSS custom properties with fallbacks
  background-color: var(--el-button-bg-color, var(--el-bg-color));
  border-color: var(--el-button-border-color, var(--el-border-color));
  color: var(--el-button-text-color, var(--el-text-color-regular));
  
  // Dynamic color calculations
  &:hover {
    background-color: var(--el-button-hover-bg-color, 
      color-mix(in srgb, var(--el-button-bg-color) 90%, var(--el-color-primary) 10%));
    border-color: var(--el-button-hover-border-color,
      color-mix(in srgb, var(--el-button-border-color) 70%, var(--el-color-primary) 30%));
  }
  
  &:active {
    background-color: var(--el-button-active-bg-color,
      color-mix(in srgb, var(--el-button-bg-color) 80%, var(--el-color-primary) 20%));
  }
  
  // Size variations using custom properties
  &.el-button--large {
    padding: var(--el-button-large-padding, var(--el-spacing-lg) var(--el-spacing-xl));
    font-size: var(--el-button-large-font-size, var(--el-font-size-large));
  }
  
  &.el-button--small {
    padding: var(--el-button-small-padding, var(--el-spacing-sm) var(--el-spacing-md));
    font-size: var(--el-button-small-font-size, var(--el-font-size-small));
  }
  
  // Type variations
  &.el-button--primary {
    background-color: var(--el-button-primary-bg-color, var(--el-color-primary));
    border-color: var(--el-button-primary-border-color, var(--el-color-primary));
    color: var(--el-button-primary-text-color, #ffffff);
  }
  
  &.el-button--success {
    background-color: var(--el-button-success-bg-color, var(--el-color-success));
    border-color: var(--el-button-success-border-color, var(--el-color-success));
    color: var(--el-button-success-text-color, #ffffff);
  }
}

// Dark theme overrides
@media (prefers-color-scheme: dark) {
  :root {
    --el-bg-color: #1a1a1a;
    --el-bg-color-page: #0a0a0a;
    --el-text-color-primary: #e4e7ed;
    --el-text-color-regular: #cfd3dc;
    --el-text-color-secondary: #a3a6ad;
    --el-border-color: #4c4d4f;
    --el-border-color-light: #414243;
    --el-border-color-lighter: #363637;
  }
}

// High contrast mode
@media (prefers-contrast: high) {
  :root {
    --el-color-primary: #0066cc;
    --el-text-color-primary: #000000;
    --el-border-color: #000000;
  }
}

// Reduced motion
@media (prefers-reduced-motion: reduce) {
  :root {
    --el-transition-duration: 0s;
    --el-transition-duration-fast: 0s;
  }
}

Best Practices

1. Design Token Organization

  • Hierarchical Structure: Organize tokens from global to component-specific
  • Semantic Naming: Use meaningful names that describe purpose, not appearance
  • Consistent Scale: Maintain consistent spacing and sizing scales
  • Accessibility: Ensure sufficient color contrast and support for accessibility preferences

2. Theme Development Guidelines

  • Mobile-First: Design themes with mobile devices in mind
  • Performance: Minimize CSS custom property usage in performance-critical areas
  • Fallbacks: Always provide fallback values for custom properties
  • Testing: Test themes across different devices and accessibility settings

3. Customization Strategies

  • Progressive Enhancement: Start with a solid base theme and add customizations
  • Component Isolation: Ensure theme changes don't break component functionality
  • Documentation: Document all available customization options
  • Validation: Validate theme configurations to prevent invalid values

Conclusion

Element Plus's advanced theme system provides:

  • Comprehensive Customization: Full control over visual appearance
  • Design System Integration: Structured approach to design tokens
  • Accessibility Support: Built-in support for accessibility preferences
  • Performance Optimization: Efficient CSS custom property usage
  • Developer Experience: Intuitive APIs and comprehensive tooling
  • Future-Proof: Extensible architecture for future enhancements

This system serves as an excellent foundation for building sophisticated design systems and theme management solutions.

Element Plus Study Guide