How to Create Custom Components That Integrate with Element UI Patterns

Extend the ElementUIComponent base class from types/component.d.ts, implement the static install method, and follow BEM naming conventions to build custom Vue components that seamlessly inherit Element UI's global configuration, event system, and visual design language.

Element UI provides a lightweight, extensible framework for building consistent Vue.js interfaces. By mirroring the internal architecture found in the ElemeFE/element repository, you can create custom components that respect global $ELEMENT settings, participate in the library's event propagation system, and maintain visual parity with native elements. This guide demonstrates the exact patterns used in the source code to ensure your custom UI elements behave as first-class citizens of the Element UI ecosystem.

Core Architecture of Element UI Components

The ElementUIComponent Base Class

Every native Element UI component derives from a shared base class defined in types/component.d.ts. This class extends Vue and provides a static install method required for Vue.use() registration, along with common type definitions for sizing and alignment.

// types/component.d.ts
export declare class ElementUIComponent extends Vue {
  static install(vue: typeof Vue): void
}

export type ElementUIComponentSize = 'large' | 'medium' | 'small' | 'mini'
export type ElementUIHorizontalAlignment = 'left' | 'center' | 'right'

By extending this class, your custom component automatically gains the ability to register globally via Vue.use() without writing boilerplate installation logic.

Global Installation and Configuration

The central plugin logic lives in src/index.js, where the library iterates over a components array and registers each one with Vue.component(). This file also establishes the global $ELEMENT configuration object that components reference for default props like size.

// src/index.js (lines 83-100)
const install = function(Vue, opts = {}) {
  components.forEach(component => {
    Vue.component(component.name, component)
  })
  Vue.use(InfiniteScroll)
  Vue.use(Loading.directive)
  Vue.prototype.$ELEMENT = { size: opts.size || '', zIndex: opts.zIndex || 2000 }
}

To bundle your custom component with the default Element UI installation, you must add it to this components array and expose a name property matching the desired tag name (e.g., 'ElMyCard' registers as <el-my-card>).

Component Naming and Event Communication

Native components define both name and componentName properties. The name property controls the global tag registration, while componentName enables the dispatch and broadcast event propagation system found in src/mixins/emitter.js.

// packages/button/src/button.vue (lines 26-30)
export default {
  name: 'ElButton',
  componentName: 'ElButton',
  // ...
}

The emitter.js mixin provides methods for communicating up and down the component tree without tight coupling, which is essential for complex components like form inputs or nested menus.

Step-by-Step Implementation Guide

1. Create the Component Skeleton

Create a single-file component that extends ElementUIComponent, respects the global size configuration, and uses BEM-style CSS classes. This example implements a MyCard component following the exact patterns from packages/button/src/button.vue.

<template>
  <div :class="cardClass">
    <slot name="header"></slot>
    <div class="my-card__body"><slot></slot></div>
    <slot name="footer"></slot>
  </div>
</template>

<script>
import { ElementUIComponent } from 'element-ui/src/types/component'

export default {
  name: 'ElMyCard',
  componentName: 'ElMyCard',
  extends: ElementUIComponent,
  props: {
    size: {
      type: String,
      validator: val => ['large', 'medium', 'small', 'mini'].includes(val)
    }
  },
  computed: {
    cardSize() {
      return this.size || (this.$ELEMENT || {}).size || ''
    },
    cardClass() {
      return [
        'el-my-card',
        this.cardSize ? `el-my-card--${this.cardSize}` : ''
      ]
    }
  }
}
</script>

<style scoped>
.el-my-card { border: 1px solid #ebeef5; border-radius: 4px; padding: 20px; }
.el-my-card--large   { font-size: 16px; }
.el-my-card--medium  { font-size: 14px; }
.el-my-card--small   { font-size: 12px; }
.el-my-card--mini    { font-size: 10px; }
</style>

Key implementation details:

  • extends: ElementUIComponent pulls in the static install method and shared type definitions
  • name: 'ElMyCard' ensures registration as <el-my-card> following the library's kebab-case convention
  • componentName enables participation in the emitter.js event system if you import the mixin
  • Size resolution follows the pattern local prop > $ELEMENT.size > default, matching the logic in native components

2. Register Your Component

You have two registration options depending on whether you are extending the library or using the component in a single project.

Individual registration (recommended for project-specific components):

import Vue from 'vue'
import MyCard from '@/components/MyCard.vue'

// Uses the inherited static install method
Vue.use(MyCard)

// Or manual registration:
Vue.component(MyCard.name, MyCard)

Bundling with Element UI (to include in the default plugin):

// src/index.js
import MyCard from '../packages/my-card/index.js'

const components = [
  // ... existing components
  MyCard
]

// Rebuild the library to include your component in Vue.use(ElementUI)

3. Usage in Templates

Once registered, the component integrates seamlessly with other Element UI elements and respects global configuration:

<template>
  <el-my-card size="large">
    <template #header>
      <h3>Custom Card Title</h3>
    </template>
    <p>This body content respects the global theme.</p>
    <template #footer>
      <el-button type="primary" @click="handleAction">Confirm</el-button>
    </template>
  </el-my-card>
</template>

<script>
export default {
  methods: {
    handleAction() {
      this.$message({ type: 'success', message: 'Custom component works!' })
    }
  }
}
</script>

The component automatically inherits the size value from Vue.use(ElementUI, { size: 'medium' }) if no local size prop is specified, maintaining consistency across your application.

Best Practices for Seamless Integration

  • Extend ElementUIComponent to ensure your component exports the required static install method and TypeScript definitions.

  • Use the El prefix in your name property (e.g., ElMyCard) to maintain consistency with the library's registration pattern and avoid naming collisions.

  • Implement the size resolution pattern by checking this.size first, then falling back to (this.$ELEMENT || {}).size, ensuring your component respects global configuration changes.

  • Define componentName if your component needs to communicate with parent or child components using the dispatch and broadcast methods from src/mixins/emitter.js.

  • Follow BEM CSS conventions with the el- prefix (e.g., el-my-card, el-my-card--large) so your styles integrate with the existing theme-chalk architecture and modifier utilities.

  • Provide flexible slots (header, default, footer) to match the compositional API pattern used by native components like el-card and el-dialog.

  • Add to the global install array in src/index.js if you need your component auto-registered when users call Vue.use(ElementUI).

Summary

  • Extend ElementUIComponent from types/component.d.ts to inherit the static install method and shared type definitions required for global registration.
  • Follow the El* naming convention and define both name and componentName properties to ensure proper tag registration and event propagation compatibility.
  • Respect global configuration by reading this.$ELEMENT for default values like size, following the precedence pattern used in packages/button/src/button.vue.
  • Use BEM CSS naming with the el- prefix to maintain visual consistency with the theme-chalk system and support size modifiers.
  • Leverage src/mixins/emitter.js when your component needs to communicate with ancestors or descendants without direct prop drilling.
  • Register via Vue.use() for individual projects, or modify src/index.js to bundle your component with the core library distribution.

Frequently Asked Questions

Do I need to modify Element UI source code to add custom components?

No. For project-specific components, you can register them individually using Vue.use(MyComponent) without touching the library source. However, to include your component in the default Vue.use(ElementUI) bundle—so it registers automatically alongside native components—you must add the import and component object to the components array in src/index.js and rebuild the library.

How does the size prop interact with global Element UI configuration?

Components should implement a computed property that checks the local size prop first, then falls back to this.$ELEMENT.size (set during Vue.use(ElementUI, { size: 'medium' })), and finally defaults to an empty string. This pattern ensures your component respects the global size setting while allowing local overrides, exactly as implemented in packages/button/src/button.vue.

Can custom components use Element UI's internal event communication system?

Yes. By importing the mixin from src/mixins/emitter.js, your component gains access to dispatch(componentName, eventName, payload) for emitting events to ancestors and broadcast(componentName, eventName, payload) for descendants. This is essential for building complex components like custom form inputs that need to notify parent forms of validation changes without explicit prop binding.

What CSS naming convention should custom components follow to match Element UI's theme?

Use BEM (Block Element Modifier) methodology with the el- namespace prefix. Define your block as el-<component-name> (e.g., el-my-card), elements as el-my-card__body, and modifiers as el-my-card--<modifier> (e.g., el-my-card--large). This ensures compatibility with the theme-chalk system and allows users to override styles using the same SCSS variables and mixins as native components.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →