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
<cai-button type="primary">
  <span slot="btnText">按钮</span>
</cai-button>
<template id="caiBtn">
  <style>
    .cai-button { display:inline-block; padding:4px 20px; font-size:14px; border:1px solid #1890ff; border-radius:2px; background-color:#1890ff; color:#fff; box-shadow:0 2px #00000004; }
    .cai-button-warning { border:1px solid #faad14; background-color:#faad14; }
    .cai-button-danger { border:1px solid #ff4d4f; background-color:#ff4d4f; }
  </style>
  <div class="cai-button"><slot name="btnText"></slot></div>
</template>
<script>
  const template = document.getElementById("caiBtn");
  class CaiButton extends HTMLElement {
    constructor() {
      super();
      this._type = { primary: 'cai-button', warning: 'cai-button-warning', danger: 'cai-button-danger' };
      const shadow = this.attachShadow({ mode: 'open' });
      const content = template.content.cloneNode(true);
      this._btn = content.querySelector('.cai-button');
      this._btn.className += ` ${this._type[this.getAttribute('type')]}`;
      shadow.appendChild(content);
    }
    static get observedAttributes() { return ['type']; }
    attributeChangedCallback(name, oldValue, newValue) {
      this[name] = newValue;
      this.render();
    }
    render() {
      this._btn.className = `cai-button ${this._type[this.type]}`;
    }
  }
  window.customElements.define('cai-button', CaiButton);
</script>

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 = `
      <style>.cai-input {}</style>
      <input type="text" id="caiInput" />
    `;
    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>
<cai-button type="primary">点击</cai-button>
<cai-input id="caiIpt"></cai-input>
<script>
  const ipt = document.getElementById('caiIpt');
  ipt.addEventListener('change', e => console.log(e.detail));
</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 (
    <div className="App">
      <cai-button type={type}>哈哈哈</cai-button>
      <cai-button onClick={() => setType('danger')}>点击</cai-button>
      <cai-input id="ipt" value={value}></cai-input>
    </div>
  );
}
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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Component 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

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.