Frontend Development 18 min read

Building a UI Component Library with Web Components

This article explains how to create reusable UI components using Web Components by covering the three core concepts—custom elements, Shadow DOM, and HTML templates—demonstrating a button example, discussing component communication, two‑way binding, library packaging, framework integrations, current limitations, and reference resources.

政采云技术
政采云技术
政采云技术
Building a UI Component Library with Web Components

As front‑end developers, we increasingly adopt component‑based development to improve reusability and reduce maintenance costs. Web Components provide a framework‑agnostic way to encapsulate UI elements that browsers can natively understand.

Three Core Elements of Web Components

Web Components consist of Custom Elements (JavaScript APIs to define new tags), Shadow DOM (isolated DOM trees for style and behavior encapsulation), and HTML Templates (reusable markup with slot for content projection). These three pillars enable building self‑contained UI widgets.

Button Component Example

// html
按钮

Elements, Lifecycle, and Example Breakdown

Custom elements : Define a class extending HTMLElement and register it with window.customElements.define('cai-button', CaiButton) .

Shadow DOM : Attach with this.attachShadow({mode: 'open'}) to keep styles and markup private.

HTML templates & slots : Use <template> to clone markup and slot to project content, similar to Vue slots.

Lifecycle callbacks : connectedCallback (mounted), disconnectedCallback (removed), adoptedCallback (moved to new document), and attributeChangedCallback (attribute changes, e.g., type ).

When a component needs to accept complex data, attributes only support strings, so we can either JSON‑stringify the data or use JavaScript properties directly. The latter allows passing objects or arrays without conversion, though property changes are not automatically observed.

Component Communication and Two‑Way Binding

To enable two‑way data flow, the component dispatches a custom change event whenever its internal state changes. Consumers listen to this event and update their data model accordingly.

// button.js (simplified)
class CaiInput extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'closed' });
    const template = document.createElement('template');
    template.innerHTML = `
`;
    const content = template.content.cloneNode(true);
    this._input = content.querySelector('#caiInput');
    this._input.value = this.getAttribute('value');
    shadow.appendChild(content);
    this._input.addEventListener('input', ev => {
      const value = ev.target.value;
      this.value = value;
      this.dispatchEvent(new CustomEvent('change', { detail: value }));
    });
  }
  get value() { return this.getAttribute('value'); }
  set value(v) { this.setAttribute('value', v); }
}
window.customElements.define('cai-input', CaiInput);

In Vue, the component can be used with :value="data" @change="e => data = e.detail" , mimicking v-model . In React, developers must attach listeners manually and use the class attribute instead of className for custom elements.

Packaging Your Own Component Library

Typical directory layout:

.
└── cai-ui
    ├── components
    │   ├── Button
    │   │   └── index.js
    │   └── ...
    └── index.js   // library entry point

Each component lives in its own file and can be imported either as a whole library or on‑demand:

// index.js (full import)
import './components/Button/index.js';
import './components/xxx/xxx.js';

// on‑demand import
import 'cai-ui/components/Button/index.js';

Theming is achieved with CSS variables, e.g., var(--primary-color, #1890ff) , allowing global color changes.

Using the Library in Different Environments

Native HTML

<script type="module" src="//cai-ui"></script>
点击

Vue 2.x

// main.js
import 'cai-ui';
export default {
  data() { return { type: 'primary', data: '' }; },
  methods: { changeType() { this.type = 'danger'; } }
};

Vue 3.x

Configure Vite to treat tags containing a hyphen as custom elements:

// vite.config.js
import vue from '@vitejs/plugin-vue';
export default {
  plugins: [
    vue({
      template: { compilerOptions: { isCustomElement: tag => tag.includes('-') } }
    })
  ]
};

React

import React, { useEffect, useState } from 'react';
import 'cai-ui';
function App() {
  const [type, setType] = useState('primary');
  const [value, setValue] = useState('');
  useEffect(() => {
    document.getElementById('ipt').addEventListener('change', e => console.log(e.detail));
  }, []);
  return (
哈哈哈
setType('danger')}>点击
);
}
export default App;

Note: React cannot automatically map class to className for custom elements, so the native class attribute must be used.

Current Drawbacks of Web Components

Primarily UI‑focused; lacks built‑in data‑driven capabilities compared to modern frameworks.

Browser and framework compatibility still requires polyfills and careful handling.

Without a framework, markup, styles, and scripts are mixed in a single file, reducing developer ergonomics.

Unit testing of Web Components is more complex than testing framework‑specific components.

Reference Documentation

Web Components – MDN: https://developer.mozilla.org/en-US/docs/Web/Web_Components

Vue 3.0 – Web Components guide: https://v3.cn.vuejs.org/guide/web-components.html

React – Web Components: https://zh-hans.reactjs.org/docs/web-components.html

Frontend Developmentcomponent libraryWeb Componentsshadow-domCustom Elements
政采云技术
Written by

政采云技术

ZCY Technology Team (Zero), based in Hangzhou, is a growth-oriented team passionate about technology and craftsmanship. With around 500 members, we are building comprehensive engineering, project management, and talent development systems. We are committed to innovation and creating a cloud service ecosystem for government and enterprise procurement. We look forward to your joining us.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.