Web Components — A Practical Perspective Using Custom Elements
Last week, I joined a live session with Ryan Carniato, the creator of Solidjs, where he discussed Web Components. It’s a topic that sparks a lot of debate, revealing just how divided opinions are on their role in modern web development.
Often, Web Components get compared to frameworks, but that approach misses the point of how this technology can be applied and leveraged today. It’s essential to recognize that different developers face different challenges, so perspectives on Web Components can vary widely based on individual experiences.
Here, I’d like to share some insights into how Web Components can be beneficial for solving real-world problems.
To start, it’s important to clarify that Web Components are not a replacement for frameworks or application libraries.
Web Components provide a set of DOM APIs that make it easier to share functionality across applications without locking into a specific framework. This is especially valuable in companies with distributed teams working across different tech stacks, allowing for a more flexible, framework-agnostic approach to reusable components.
You don’t need to use every feature of Web Components at once.
In fact, I find the Custom Elements API to be the most valuable of all the Web Components APIs available today. It’s incredibly versatile and useful in many scenarios. Moving forward, I’ll focus solely on this feature and use it as my definition of a Web Component.
1. Web Components are the Real "Micro Front-ends"
I don’t know about you, but I feel the frontend community has taken Micro Frontends a bit too far. Many implementations of Micro Frontends seem unnecessarily complex; just hearing about Module Federation gives me the creeps!
Micro Frontends should be self-contained, focused modules with a limited, well-defined scope.
Here are a few examples where Web Components are especially well-suited:
- Self-contained widgets
- Chat widgets
- General-purpose carousels
- Like/unlike buttons
The Custom Elements API provides lifecycle callbacks that enable you to register and unregister events, making it easier to manage multiple instances of components. Remember when the internet was flooded with Facebook Like buttons, all implemented as iframes?
For those of us who have created custom carousels, you might recall needing to implement .destroy() methods to handle unmounting instances. With the Custom Elements lifecycle, you can now leverage its built-in methods alongside other custom elements to manage slides, eliminating the need for specific class names to identify and query them.
I wouldn’t recommend using Web Components for small UI elements like buttons, as that’s better suited for a design system’s CSS. The true strength of Web Components lies in their ability to share behavior and functionality. It’s important not to create custom elements for everything; you’ll soon find it’s not the best approach.
The Like button is a prime example, as it involves more than just UI — there’s functionality like maintaining a counter and making API requests. For cases like the Like button and carousels, I would write them in vanilla JavaScript using the Custom Elements API. Thanks to modern browser capabilities and language features, implementing these components has become significantly easier.
2. Agnostic Shell Applications
You may need to integrate full applications developed by different teams within a Single Page Application (SPA). Some teams refer to this approach as a Micro Frontends architecture. I don't consider a distributed Single Page Application as Micro Frontend because it's beyond a "Micro" scope.
Custom Elements can serve as an effective entry point for your App Shell. By creating an application outlet, such as <shell-outlet/>, you can route by URL to determine which Web Component to mount inside the outlet, dynamically loading the necessary JavaScript from external resources. This approach promotes modularity and flexibility, allowing different teams to develop and maintain their components independently.
You can use Custom Elements as both an interface and a common entry point for each application within a shell, enabling integration with your preferred framework. This approach establishes a standardized method for incorporating apps into your shell, making the integration process more predictable. The possibilities with <shell-outlet/> are vast, allowing for creative implementations.
Here’s a very simple and naive implementation of that idea:
class ShellOutlet extends HTMLElement {
constructor() {
super();
this.currentComponent = null;
}
connectedCallback() {
this.render();
window.addEventListener('popstate', () => this.render());
}
render() {
const route = window.location.pathname;
this.loadComponent(route);
}
async loadComponent(route) {
// Clean up the previous component
if (this.currentComponent) {
this.currentComponent.remove();
}
// Determine which component to load based on the route
let componentName;
switch (route) {
case '/home':
componentName = 'home-component';
break;
case '/about':
componentName = 'about-component';
break;
default:
componentName = 'not-found-component';
}
// Dynamically import the component
const module = await import(`./${componentName}.js`);
this.currentComponent = document.createElement(componentName);
this.appendChild(this.currentComponent);
}
}
customElements.define('shell-outlet', ShellOutlet);
Generated by Chat-GPT =)
You could use different frameworks to mount on a specific Custom Element if you will.
3. Application Library Creators
As a developer, I was often dissatisfied with the alternatives available when React and other frameworks emerged. They tended to be overly complex, locking you into their ecosystems and creating tightly intertwined applications burdened by their own complexities.
Years ago, I created the Jails project, inspired by the Ruby on Rails framework. My goal was to develop a framework that integrated seamlessly with the language, much like Rails did. While it wasn’t meant to be a direct port of Rails, I drew inspiration from its philosophy.
Before the advent of Custom Elements, I had to rely on numerous creative workarounds to track when a component was mounted or unmounted in the DOM. I used the MutationObserver API to monitor the <body />, inferring when to initiate hydration and query node elements tagged with data-component. This approach resulted in a lot of code and complexity, making the system prone to errors.
With the introduction of Custom Elements, I was able to significantly reduce that complexity in the Jails source code. Custom Elements provided a more straightforward way to identify components, using custom HTML element names instead of relying on data attributes. With the define method, components are registered once, allowing the browser to manage instances and lifecycle callbacks.
Thus, Web Components technologies can serve as powerful tools for framework creators, especially for those looking to build closer to standards while enhancing interoperability and ease of use.
An important piece of advice: Do not use Web Components solely for dynamic HTML rendering purposes.
I’ve seen many instances where developers create a “React clone” using Web Components and share it within design systems. One of the most common use cases for this approach is dynamic buttons.
<ui-button icon="home" icon-position="left">
Home Page
</ui-button>
That's an attempt to recreate a rendering UI Component like React does:
export const UIButton = ({ icon, iconPosition }) => {
return (
<button>
{ iconPosition == 'left'? <Icon name={icon} />: null }
{children}
{ iconPosition == 'left'? <Icon name={icon} />: null }
</button>
)
}
const Icon = ({ name }) => {
return <span className=`icon icon-${name}`></span>
}
React was originally designed as a rendering UI library, primarily intended for use in Single Page Applications (SPAs). In an SPA context, everything is rendered on the client side using JavaScript. However, when working with Web Components, these components can be utilized in various environments, including Static Site Generators (SSGs) and Server-Side Rendering (SSR) sites.
You don’t want your site or app to rely entirely on JavaScript for everything, especially for rendering simple elements like buttons. In fact, this dependency is often a downside of using frameworks like React. React utilizes JSX to interpolate HTML with data, functioning as a “template system” from a developer experience perspective. On the other hand, Web Components lack a native template system for this interpolation, which can complicate the process.
Therefore, my advice is to share a Design System with static HTML documentation that different teams can use as a reference and CSS library to reuse UI styles. This allows each team to decide how they want to render dynamic HTML, providing flexibility while maintaining a consistent design across the application. Example:
Wouldn’t that lead to duplication of components across different teams, resulting in a lack of reuse?
You will be reusing CSS, which is already a significant advantage. We often spend more time on CSS than you might think, trust me.
Reusability is important, but it comes with a very high cost: The Coupling. So be careful with what your team decides what is worth reusing. You might be surprised by how often your work can be impacted by aligning with external resources managed by other teams.
Some components are “local,” meaning only Team A will use them, while others are more “global.” For those global components, you can get creative with your organizational structure to determine which team should be responsible for shared components.
One important point to note is that shared components should not be the responsibility of the Design System team.
Conclusion
Web Components could be more widely adopted as a solution for distributing functionality within companies. To me, they represent a “low-level API” that isn’t intended for every situation and certainly shouldn’t be used as an application library or framework.
However, they can be extremely useful for integrating systems and sharing functionality. For those developing UI libraries in vanilla JavaScript, like Chart.js and Swiper, there’s no reason not to implement them as Web Components.
I’m not a fan of the existing Web Components libraries; they often mirror the complexity of many frameworks, becoming confusing and overloaded with features. I believe we should shift away from this complex mindset and consider alternative perspectives.