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.