Frontend Development

Building a Framework-Agnostic UI Kit: A Practical Guide to Web Components

In the modern frontend ecosystem, reliance on specific frameworks like React, Vue, or Angular is ubiquitous. However, there is a growing need for UI components that transcend these boundaries. Whether you are maintaining a legacy jQuery application, building a micro-frontend architecture, or simply want to ensure your design system remains portable, Web Components offer a native, standards-based solution. This guide explores how to build a robust, framework-agnostic UI kit using Custom Elements and the Shadow DOM.

The Core Philosophy: Encapsulation and Interoperability

Web Components are not a silver bullet for every problem, but they excel at creating isolated, reusable pieces of UI. The technology relies on three main pillars: Custom Elements, which allow you to define new HTML tags; the Shadow DOM, which provides scoped styling and markup encapsulation; and HTML Templates, which enable efficient DOM manipulation.

By leveraging these APIs, you can create components that look and behave consistently across different browsers and frameworks without the overhead of virtual DOM diffing or framework-specific lifecycle hooks.

Step 1: Defining the Custom Element

The foundation of any Web Component is the Custom Element class. This class extends HTMLElement and defines the component's behavior, lifecycle, and attributes. Let's start by building a simple, reusable app-button component.

class AppButton extends HTMLElement {
  constructor() {
    super();
    // Attach shadow root for encapsulation
    this.attachShadow({ mode: 'open' });
    this.render();
  }

  // React to attribute changes
  static get observedAttributes() {
    return ['variant', 'disabled'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.render();
    }
  }

  // Render the component's internal DOM
  render() {
    const variant = this.getAttribute('variant') || 'primary';
    const disabled = this.hasAttribute('disabled');

    this.shadowRoot.innerHTML = `
      
      
    `;
  }
}

// Register the component
customElements.define('app-button', AppButton);

In this example, we use static get observedAttributes() to tell the browser which attributes should trigger a re-render. The attributeChangedCallback ensures that our UI stays in sync with the HTML attributes passed to the component.

Step 2: Mastering Shadow DOM and Styling

The Shadow DOM is where the magic of encapsulation happens. Styles defined inside :host or within the shadow tree do not leak out, and external styles do not break in. This is crucial for UI kits because it prevents CSS conflicts in larger applications.

However, you may need to allow consumers of your UI kit to style specific parts. This is where the <slot> element comes in. In the code above, we used <slot> inside the button to allow users to pass in custom content, such as icons or text, while keeping the button structure intact.

Step 3: Handling Events in an Isolated Environment

One common pitfall for beginners is event handling inside the Shadow DOM. Events that originate inside the shadow tree (like clicking a button) are not automatically accessible in the light DOM (the parent component) unless they are composed. You can control this behavior by setting the composed: true flag on your custom events.

handleClick() {
  this.dispatchEvent(new CustomEvent('click', {
    bubbles: true,
    composed: true, // Allows event to escape shadow DOM
    detail: { action: 'submit' }
  }));
}

By setting composed: true, you ensure that the parent application can listen for events fired by your component, maintaining interoperability.

Conclusion

Building a framework-agnostic UI kit using Web Components is a strategic investment in long-term maintainability. It reduces coupling, simplifies testing, and ensures your design system remains future-proof. While the learning curve involves understanding encapsulation boundaries, the payoff is a set of highly portable, performant, and consistent UI primitives. Start small with a single component, experiment with the Shadow DOM, and gradually expand your kit to cover your entire design system.

Share: