Joseph Carothers
Web Component UI Library

Web Component UI Library

Wed Sep 25 2024

Building a Custom Web Component UI Library: The Power of Native Web Technologies

In my quest to build a versatile, cross-platform UI component library, I stumbled into the world of Web Components. While I’ve built many tools using frameworks like React and Vue, this time I wanted something more flexible, something that could easily plug into any environment—from WordPress to a custom React application—without much overhead. Enter Web Components.

This post is about my journey building a custom component library using native browser technologies. Along the way, I inadvertently recreated some familiar patterns, like React's class components, and discovered the real power of native Web Components. Here’s a deep dive into what I built, why I built it, and how you can leverage similar tools in your own projects.

What Are Web Components?

At their core, Web Components are a set of APIs that let you create reusable, encapsulated HTML elements. Unlike traditional front-end frameworks, Web Components run natively in the browser, which means they don’t require extra libraries or frameworks.

There are three key technologies behind Web Components:

For a more in-depth overview, you can check out MDN’s Web Components guide.

The Problem I Set Out to Solve

This library was born out of a practical need: I wanted a flexible, reusable UI component library that could work seamlessly across different platforms—whether the client was using WordPress, a custom CMS, or a React-based application. The components needed to be lightweight, modular, and highly configurable.

I also wanted to avoid re-inventing the wheel with every new project or client request. Web Components, with their native encapsulation and reusability, were a perfect fit for this scenario.

Web Component Lifecycle: A Familiar Friend

If you’re used to React or Vue, Web Components might feel surprisingly familiar. They come with their own lifecycle methods, much like React’s componentDidMount, componentDidUpdate, and componentWillUnmount.

Here’s a quick comparison:

| Web Component Callback | React Equivalent | Description | | -------------------------- | ---------------------- | --------------------------------------------------------------------------- | | connectedCallback | componentDidMount | Runs when the element is added to the DOM—great for initial setup. | | disconnectedCallback | componentWillUnmount | Runs when the element is removed—useful for cleanup. | | attributeChangedCallback | componentDidUpdate | Fires when observed attributes change—ideal for responding to prop changes. |

These lifecycle methods allow you to manage state, re-render the component, and handle clean-up in a manner very similar to how you would in React. But unlike React, there’s no virtual DOM here—Web Components manage their own rendering.

Example: connectedCallback

connectedCallback() {
  // Set up initial state, listeners, or make API calls here
  this.render();
}

Example: attributeChangedCallback

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

Creating State with Web Components

One of the main challenges I faced was managing state within my custom elements. Web Components don’t have a built-in state management system like React’s useState or Vue’s data. To handle this, I created a simple state system within each component that manages internal data and re-renders the component when necessary.

A Simple setState Example

Here’s how state management works in the components:

setState(newState) {
  this.state = { ...this.state, ...newState };
  this.render();
}

This method allows the component to update its internal state and automatically trigger a re-render whenever that state changes.

Styling with Shadow DOM

The Shadow DOM is one of the coolest parts of Web Components. It allows you to encapsulate the styles and logic of each component, so your styles don’t leak out to the rest of the page—and external styles won’t affect your component.

Here’s how I used the Shadow DOM in the project:

class MyComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        /* Scoped styles live here */
        .my-component {
          background: blue;
          color: white;
        }
      </style>
      <div class="my-component">
        Hello from the Shadow DOM!
      </div>
    `;
  }
}

With this setup, the component is fully encapsulated—none of its styles will bleed into the global scope, and it remains completely self-contained.

Handling External Data with Web Components

One of the key features I needed was the ability to handle external data. Whether it’s updating live scores or rendering promotional content dynamically, Web Components allowed me to seamlessly integrate support. Here’s a simple example:

async connectedCallback() {
  data = await fetchData(this.dataId)

  this.setState({ data })
}

This simple strategy of fetching on connected allows us fetch data before the content is rendered. Alternatively we can move this to a later stage or have a deferred attribute to defer the fetch.

Key Lessons Learned

Final Thoughts

Building this Web Component UI library was an eye-opening experience. It reinforced my belief in the power of native browser technologies and the flexibility they offer. While frameworks like React and Vue are fantastic for most use cases, Web Components shine when you need something lightweight, encapsulated, and reusable across many different platforms.

If you’re curious to explore more, check out the GitHub repository, and feel free to fork the project or contribute!

Happy coding!