Do we need Web Components?

Bernard Lekaj
ReversingLabs Engineering
9 min readApr 4, 2023

--

Source: photo @ unsplash

Introduction

About 10 years ago, the first draft version of the Web Component specification was written describing the basic concepts behind it. Initially written by Google and later built upon by WHATWG and published as a v1 version, it brought new standards and features to the web. The motivation behind these specifications is to provide a way to build custom, fully-featured HTML elements and rationalize the platform so that future features and standards work as one coherent system. With today’s web development being focused on components and reusability, these technologies aim to support modern development and help in the standardization along the way.

Web components consist of three main technologies with tools to create customizable custom components. They provide a level of encapsulation that gives us highly reusable components without fear of code collision. The following is the description of each of these concepts:

  • Custom elements: Reusable elements with custom behavior used to build the user interface; they can extend the current HTML elements or be defined as completely new custom elements,
  • Shadow DOM: Encapsulated part of the component tree rendered separately from the main document DOM, and has its own private styles and behavior which gives us the ability to hide the internals of the component from the rest of the DOM, while still providing the same functionality as it was originally designed for,
  • HTML templates: HTML fragment contained inside a <template> element not rendered on the page, but can be used as the basis for a custom element’s structure; elements can also have placeholders defined with <slot> elements that can be later populated with your own markup.

These technologies are natively available in modern browsers and can be supported in older browsers through polyfills. Web components can also be used in conjunction with libraries and frameworks like React, Vue and others. That, and the fact that companies like GitHub, Google and others use them in their flagship products, makes it an exciting prospect.

Web components crash course

To answer the question of do we need them or not, we will first try to get a better understanding how to use Web components and how they work. So lets go through a few examples. We will start by defining a class that is going to contain the logic behind our custom element (these elements are referred to as autonomous custom elements). Furthermore, we are going to define a template and slots to define how the element is going to be rendered. And finally, for this class to be used, it must be registered in the CustomElementRegistry, which is available as a global variable under the name “customElements”.

The end result is going to look something like this:

Example Web Component

For this example we are going to create two files: main.js, which contains the custom class and registration of the custom element, and index.html, which contains the template and the link to our main.js script.

Contents of main.js:

customElements.define('person-details', // customElements is a global variable representing CustomElementRegistry, and in this step we imediatelly register the custom element named person-details
class extends HTMLElement {
constructor() {
super();

const template = document.getElementById('person-template'); // use the template defined in index.html
const templateContent = template.content;

const shadowRoot = this.attachShadow({mode: 'open'}); // add the shadow root

const style = document.createElement('style'); // add the styles
style.textContent = `
div { padding: 10px; border: 1px solid gray; width: 200px; margin: 10px; }
h2 { margin: 0 0 10px; }
ul { margin: 0; }
p { margin: 10px 0; }
::slotted(*) { color: gray; font-family: sans-serif; }
`; // slotted is a CSS pseudo-element that represents any element placed into a slot

shadowRoot.appendChild(style); // append styles to shadow root
shadowRoot.appendChild(templateContent.cloneNode(true)); // append
}
});

Contents of index.html:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>::slotted example</title>
<script src="main.js" defer></script>
</head>
<body>

<template id="person-template">
<div>
<h2>Personal ID Card</h2>
<!-- slot elements designate sections that are later populated in custom element definition -->
<slot name="person-name">NAME MISSING</slot>
<ul>
<li>
<slot name="person-age">AGE MISSING</slot>
</li>
<li>
<slot name="person-occupation">OCCUPATION MISSING</slot>
</li>
</ul>
</div>
</template>

<person-details>
<!-- elements with the slot attribute are going to populate the above slot elements -->
<p slot="person-name">Morgan Stanley</p>
<span slot="person-age">36</span>
<span slot="person-occupation">Accountant</span>
</person-details>

</body>
</html>

Existing HTML elements can also be extended with custom functionality (these elements are referred to as customized built-in elements). When doing this, the registration and definition of the element looks a bit different than creating a new element. The main difference is that the element that is extended needs to be explicitly defined. Let’s take a look at how we would extend the native HTML button.

In this example we will extend the native button with a confirm dialog that asks the user if they want to proceed with the current action.

Contents of the main.js:

class ConfirmButton extends HTMLButtonElement { // notice it extends HTMLButtonElement
constructor() {
super();

this.addEventListener('click', e => {
const result = confirm(this.confirmText);

if (!result) {
e.preventDefault();
}
});
}

get confirmText() {
return this.getAttribute("confirm-text");
}

}

// notice that it indicates that it extends the button element
customElements.define('confirm-button', ConfirmButton, {extends: "button"});

Contents of the index.html:

<!-- notice the difference that it is still a button element but with the attribute "is" set to the custom element -->
<button is="confirm-button" confirm-text="Do you want to proceed?">
Click to confirm
</button>

Prototyping new elements is very easy with Web components since the API is natively supported by the browser. To view the rendered components you can simply open it in a browser. To develop production-ready components, you would at least minify them or use Typescript or similar, which then requires build tools that provide those services. But the ability to immediately see the results is a great benefit for simple prototyping and testing.

Web components have their own lifecycle represented by callback class methods invoked at different points of the element’s lifecycle:

  • connectedCallback: invoked when the custom element is added into the DOM (also called each time the node is moved); similar to React’s componentDidMount or Angulars ngOnInit,
  • disconnectedCallback: invoked each time the custom element is removed from the DOM; similar to React’s componentWillUnmount or Angular’s ngOnDestroy;
  • attributeChangedCallback: invoked each time the custom component’s attribute is changed in the DOM, but only for attributes that are defined in the observedAttributes property; similar to React’s componentDidUpdate or Angular’s ngOnChanges,
  • adoptedCallback: invoked each time the custom element is moved to a new document.

As we can see, the callbacks that are available in modern web development libraries/frameworks are also available in Web components. Most use cases can be directly translated by moving, for example, componentDidMount to connectedCallback, componentWillUnmount to disconnectedCallback, etc. A simple example comparing Web components and React’s lifecycle callbacks would look something like this:

class LifecycleMethods extends HTMLElement {
// Specify observed attributes so that
// attributeChangedCallback will work
static get observedAttributes() {
return ['value', 'disabled'];
}

constructor() {
// Always call super first in constructor
super();

console.log('Custom element constructor called.');
}

connectedCallback() {
console.log('Custom element added to page.');
}

disconnectedCallback() {
console.log('Custom element removed from page.');
}

adoptedCallback() {
console.log('Custom element moved to new page.');
}

attributeChangedCallback(name, oldValue, newValue) {
console.log('Custom element attributes changed.');
}
}

customElements.define('lifecycle-methods', LifecycleMethods);

Usability and reusability of Web components

Alongside the ability to use Web components natively, Web components give us a great level of flexibility when it comes to building applications. In case of an application built entirely on pure JavaScript, maybe using jQuery or similar, it is easy to gradually move parts of the application by just moving smaller features into Web components for a small price of refactoring. The additional benefit is not having to add additional libraries, configurations, build steps, etc. to do so.

Even if at some point we decide we want to rewrite the app to React, we still have the option to use the same refactored Web components. This makes the price of switching technologies much lower than before, which makes the application more resilient to library/framework deprecation and vendor lock-in. Vendor lock-in refers to a situation where the cost of switching to a different library or framework is higher than the cost of maintaining the application with the current technology, and the application is essentially stuck using it.

Most of the current popular libraries and frameworks support using Web components to some degree. React has documented some examples of how to use Web components inside a React application and how to use React inside a Web component application (link to the docs). Something similar can also be done with Angular with the @angular/elements package that enables developers to package Angular components as custom elements, and Vue enables creating Web components through the defineCustomElement API.

When it comes to supporting older browsers, there are official polyfills that can be included so that Web components can be used in the same way as in newer browsers. The official polyfills are available here.

State of Web components

Modern browsers mostly support Web components with the Custom Elements API supported by 96% of browsers globally (18% with partial support), HTML templates supported by about 97% of browsers globally, and Shadow DOM API supported by about 96% of browsers globally. As previously said, official polyfills can be used to support these APIs in browsers that do not natively support them. All compatibility data can be found here and here.

Another interesting statistic is the percentage of pages loaded that registered at least one Web component (although this number is only from Chrome browsers) which shows that the usage of this API has doubled in the last year or so:

When it comes to big tech firms, they have already started deploying their own Web components with GitHub being a great example. In the last few years, they have been moving from jQuery to Web components and have built many reusable elements that they also publish for others to use. These components are available here, as well as a number of components from the rest of the community. One of the reasons why GitHub’s transition to Web components has gone well is that they defined a set of patterns and techniques for developing components called Catalyst. In its core, it is a simple library that provides developers with utility functions that help with development by reducing boilerplate code. Another reason is that they’ve created a linter that is used along the generic Javascript linter.

Other big tech firms like Apple, Amazon, Google, NASA, Salesforce and many others have also started using Web components (the whole list can be seen here). These firms do not necessarily use pure Web components, but libraries or frameworks built on top of them. For example, Apple uses Stencil.js for sections of their Apple Music platform. There are many libraries and frameworks built on top of Web components with Google’s Lit being one of the most popular ones. Some of the other ones are: FASTElement, snuggsi, X-Tag, Slim.js, Smart, Stencil, hyperHTML-Element, DataFormsJS, and Custom-Element-Builder.

Component libraries are good candidates where Web components would be very useful. The reason is that they contain simple UI components which can easily be implemented with Web components. The great benefit is that these libraries would be implemented only once, as Web components, and could be used alongside a variety of technologies like React, Vue, Next and others. For example, Material UI as a design system is implemented in multiple technologies so that developers can include them in their applications. So, a React application usually includes a React implementation of Material UI to render these components. Web components could help consolidate these kinds of libraries into one which would greatly help with maintaining them, managing issues, new features, etc.

Conclusion

Even though Web components show great potential for the future, they are still in their early stages. Even though some things are still simpler to implement in other technologies like React and Vue, the great thing with Web components is that they mix and match with almost any of the existing libraries, making a lot of things easier for developers. Standardizing Web technologies like HTML, CSS and SVG, and making them work as a unit is always a good thing in the long run. With big companies already adopting Web components as first citizens in their own projects, it should at least put them on your radar as well.

If you want to find out more here are some useful links for further reading:

--

--