Overall Architecture and Design Philosophy
Overview
Element Plus is a Vue 3-based component library that follows modern frontend development principles and design patterns. This document explores the overall architecture and design philosophy that makes Element Plus a robust and scalable UI framework.
Design Philosophy
1. Consistency
Element Plus maintains visual and behavioral consistency across all components:
- Visual Consistency: Unified color schemes, typography, spacing, and visual hierarchy
- Behavioral Consistency: Consistent interaction patterns and user experience
- API Consistency: Uniform naming conventions and component interfaces
2. Efficiency
The library is designed for developer efficiency and performance:
- Developer Experience: Intuitive APIs and comprehensive documentation
- Build Efficiency: Tree-shaking support and modular architecture
- Runtime Performance: Optimized rendering and minimal bundle size
3. Controllability
Provides fine-grained control over component behavior:
- Customization: Extensive theming and styling options
- Configuration: Flexible component configuration
- Extension: Easy to extend and customize components
Architectural Principles
1. Composition over Inheritance
<!-- Good: Composition-based approach -->
<template>
<el-button :loading="loading" @click="handleClick">
<el-icon><Plus /></el-icon>
Add Item
</el-button>
</template>
<!-- Components are composed together rather than inherited -->
2. Single Responsibility
Each component has a clear, single purpose:
<!-- Button component focuses only on button functionality -->
<el-button type="primary" size="large">
Primary Button
</el-button>
<!-- Icon component focuses only on icon display -->
<el-icon size="20">
<Edit />
</el-icon>
3. Prop-driven Configuration
Components are configured through props rather than global state:
<template>
<el-table
:data="tableData"
:loading="loading"
:stripe="true"
:border="true"
height="400"
>
<el-table-column prop="name" label="Name" />
<el-table-column prop="age" label="Age" />
</el-table>
</template>
Core Architecture
1. Component Layer Structure
Element Plus Architecture
├── Theme Layer (CSS Variables, SCSS)
├── Component Layer (Vue Components)
├── Composables Layer (Reusable Logic)
├── Utils Layer (Helper Functions)
└── Types Layer (TypeScript Definitions)
2. Component Anatomy
<!-- Typical Element Plus Component Structure -->
<template>
<div :class="componentClasses" :style="componentStyles">
<slot name="prefix" />
<div class="el-component__content">
<slot />
</div>
<slot name="suffix" />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useNamespace } from '@element-plus/hooks'
// Props definition
interface ComponentProps {
size?: 'large' | 'default' | 'small'
type?: 'primary' | 'success' | 'warning' | 'danger'
disabled?: boolean
}
const props = withDefaults(defineProps<ComponentProps>(), {
size: 'default',
type: 'primary',
disabled: false
})
// Namespace for CSS classes
const ns = useNamespace('component')
// Computed properties
const componentClasses = computed(() => [
ns.b(), // Base class
ns.m(props.type), // Modifier class
ns.is('disabled', props.disabled) // State class
])
const componentStyles = computed(() => ({
// Dynamic styles
}))
</script>
3. Theming Architecture
// CSS Variables for theming
:root {
// Color system
--el-color-primary: #409eff;
--el-color-success: #67c23a;
--el-color-warning: #e6a23c;
--el-color-danger: #f56c6c;
// Typography
--el-font-size-base: 14px;
--el-font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimSun, sans-serif;
// Spacing
--el-spacing-base: 4px;
// Border radius
--el-border-radius-base: 4px;
}
// Component-specific variables
.el-button {
--el-button-font-size: var(--el-font-size-base);
--el-button-border-radius: var(--el-border-radius-base);
--el-button-padding-vertical: 8px;
--el-button-padding-horizontal: 15px;
}
Design Tokens
1. Color System
// Color palette structure
interface ColorPalette {
primary: {
50: string
100: string
200: string
// ... up to 900
DEFAULT: string
}
success: ColorScale
warning: ColorScale
danger: ColorScale
info: ColorScale
}
// Usage in components
const buttonColors = {
primary: 'var(--el-color-primary)',
success: 'var(--el-color-success)',
warning: 'var(--el-color-warning)',
danger: 'var(--el-color-danger)'
}
2. Typography Scale
// Typography system
$font-sizes: (
'extra-large': 20px,
'large': 18px,
'medium': 16px,
'base': 14px,
'small': 13px,
'extra-small': 12px
);
$line-heights: (
'extra-large': 28px,
'large': 26px,
'medium': 24px,
'base': 22px,
'small': 20px,
'extra-small': 18px
);
3. Spacing System
// Spacing scale based on 4px grid
$spacings: (
'xs': 4px, // 1 unit
'sm': 8px, // 2 units
'md': 12px, // 3 units
'lg': 16px, // 4 units
'xl': 20px, // 5 units
'2xl': 24px, // 6 units
'3xl': 32px, // 8 units
'4xl': 40px // 10 units
);
Component Communication Patterns
1. Props Down, Events Up
<!-- Parent Component -->
<template>
<el-form :model="form" @submit="handleSubmit">
<el-form-item label="Name">
<el-input
v-model="form.name"
@input="handleNameChange"
@blur="validateName"
/>
</el-form-item>
</el-form>
</template>
<script setup>
const form = reactive({ name: '' })
const handleSubmit = (formData) => {
// Handle form submission
}
const handleNameChange = (value) => {
// Handle name input change
}
const validateName = () => {
// Validate name field
}
</script>
2. Provide/Inject for Deep Communication
<!-- Form Component (Provider) -->
<script setup>
import { provide } from 'vue'
const formContext = {
model: formModel,
rules: formRules,
validateField: (field) => { /* validation logic */ }
}
provide('elForm', formContext)
</script>
<!-- Form Item Component (Consumer) -->
<script setup>
import { inject } from 'vue'
const formContext = inject('elForm')
const validateCurrentField = () => {
formContext.validateField(props.prop)
}
</script>
3. Composables for Shared Logic
// useFormValidation composable
export function useFormValidation(rules: FormRules) {
const errors = ref<Record<string, string>>({})
const isValid = computed(() => Object.keys(errors.value).length === 0)
const validateField = (field: string, value: any) => {
const fieldRules = rules[field]
if (!fieldRules) return true
for (const rule of fieldRules) {
const result = rule.validator(value)
if (!result.valid) {
errors.value[field] = result.message
return false
}
}
delete errors.value[field]
return true
}
const validateForm = (formData: Record<string, any>) => {
let valid = true
for (const field in formData) {
if (!validateField(field, formData[field])) {
valid = false
}
}
return valid
}
return {
errors: readonly(errors),
isValid,
validateField,
validateForm
}
}
Performance Considerations
1. Tree Shaking Support
// Individual component imports
import { ElButton } from 'element-plus'
import { ElInput } from 'element-plus'
// Or specific imports
import ElButton from 'element-plus/es/components/button'
import ElInput from 'element-plus/es/components/input'
2. Lazy Loading
<script setup>
import { defineAsyncComponent } from 'vue'
// Lazy load heavy components
const ElTable = defineAsyncComponent(() =>
import('element-plus/es/components/table')
)
const ElDatePicker = defineAsyncComponent(() =>
import('element-plus/es/components/date-picker')
)
</script>
3. Virtual Scrolling
<!-- For large datasets -->
<template>
<el-table-v2
:columns="columns"
:data="data"
:width="700"
:height="400"
fixed
/>
</template>
Accessibility Architecture
1. ARIA Support
<template>
<button
:class="buttonClasses"
:aria-disabled="disabled"
:aria-pressed="pressed"
:aria-label="ariaLabel"
role="button"
>
<slot />
</button>
</template>
2. Keyboard Navigation
// Keyboard navigation composable
export function useKeyboardNavigation(items: Ref<any[]>) {
const activeIndex = ref(0)
const handleKeydown = (event: KeyboardEvent) => {
switch (event.key) {
case 'ArrowDown':
event.preventDefault()
activeIndex.value = Math.min(activeIndex.value + 1, items.value.length - 1)
break
case 'ArrowUp':
event.preventDefault()
activeIndex.value = Math.max(activeIndex.value - 1, 0)
break
case 'Enter':
case ' ':
event.preventDefault()
// Activate current item
break
}
}
return {
activeIndex,
handleKeydown
}
}
3. Focus Management
// Focus trap for modals
export function useFocusTrap(containerRef: Ref<HTMLElement | null>) {
const focusableElements = computed(() => {
if (!containerRef.value) return []
return Array.from(
containerRef.value.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
) as HTMLElement[]
})
const trapFocus = (event: KeyboardEvent) => {
if (event.key !== 'Tab') return
const elements = focusableElements.value
if (elements.length === 0) return
const firstElement = elements[0]
const lastElement = elements[elements.length - 1]
if (event.shiftKey) {
if (document.activeElement === firstElement) {
event.preventDefault()
lastElement.focus()
}
} else {
if (document.activeElement === lastElement) {
event.preventDefault()
firstElement.focus()
}
}
}
return { trapFocus }
}
Testing Architecture
1. Unit Testing
// Component unit test
import { mount } from '@vue/test-utils'
import { ElButton } from 'element-plus'
describe('ElButton', () => {
it('renders correctly', () => {
const wrapper = mount(ElButton, {
props: {
type: 'primary'
},
slots: {
default: 'Click me'
}
})
expect(wrapper.classes()).toContain('el-button--primary')
expect(wrapper.text()).toBe('Click me')
})
it('emits click event', async () => {
const wrapper = mount(ElButton)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toHaveLength(1)
})
})
2. Integration Testing
// Form integration test
import { mount } from '@vue/test-utils'
import { ElForm, ElFormItem, ElInput, ElButton } from 'element-plus'
describe('Form Integration', () => {
it('validates form correctly', async () => {
const wrapper = mount({
template: `
<el-form ref="form" :model="form" :rules="rules">
<el-form-item prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-button @click="submit">Submit</el-button>
</el-form>
`,
data() {
return {
form: { name: '' },
rules: {
name: [{ required: true, message: 'Name is required' }]
}
}
},
methods: {
async submit() {
await this.$refs.form.validate()
}
}
}, {
global: {
components: { ElForm, ElFormItem, ElInput, ElButton }
}
})
await wrapper.find('button').trigger('click')
expect(wrapper.find('.el-form-item__error').exists()).toBe(true)
})
})
Build and Distribution
1. Module Formats
// ESM (ES Modules)
import { ElButton } from 'element-plus'
// CommonJS
const { ElButton } = require('element-plus')
// UMD (Browser)
// <script src="element-plus/dist/index.full.js"></script>
// const { ElButton } = ElementPlus
2. CSS Distribution
// Full CSS bundle
@import 'element-plus/dist/index.css';
// Individual component CSS
@import 'element-plus/es/components/button/style/css';
// SCSS source
@import 'element-plus/theme-chalk/src/index.scss';
Best Practices
1. Component Design
- Keep components focused and single-purpose
- Use composition over inheritance
- Provide sensible defaults
- Make components configurable through props
- Follow consistent naming conventions
2. Performance
- Implement tree-shaking support
- Use lazy loading for heavy components
- Optimize bundle size
- Implement virtual scrolling for large datasets
3. Accessibility
- Include ARIA attributes
- Support keyboard navigation
- Provide focus management
- Ensure color contrast compliance
- Test with screen readers
4. Maintainability
- Write comprehensive tests
- Document component APIs
- Use TypeScript for type safety
- Follow consistent code style
- Implement proper error handling
Conclusion
Element Plus's architecture is built on solid principles of consistency, efficiency, and controllability. The modular design, comprehensive theming system, and focus on developer experience make it a robust choice for Vue 3 applications. Understanding these architectural principles helps developers make the most of the library and contribute to its ecosystem effectively.