Skip to main content

Web components: The ultimate guide

Web components: Characters © iStock / Getty Images Plus; aurielaki
(Image credit: Characters © iStock / Getty Images Plus; aurielaki)

With web components, developers now have the ability to create their own HTML elements. In this ultimate guide, you'll learn everything you need to know. On this page, we'll cover what exactly web components are, the benefits of using them, and the elements that make them up. 

Then you'll learn how to build your own components in two short tutorials – on page 2 you'll learn how to building a component with the HTML templates and shadowDOM APIs, and on page 3 you'll got one step further and learn how to build a customised, built-in element.

Finally on page 4 you'll find some frameworks and tools to get you started, and examples of existing elements to try to today. 

While you're here, you might also want to take a look at our guide to the top HTML APIs, and how to use them.

What are web components?

Developers love components. They are a great way to define blocks of code that can be dropped in and reused anywhere they are required. Over the years, there have been a few attempts to convey this idea on the web, with varying degrees of success.

Mozilla's XML Binding Language and Microsoft's HTML Component specifications date back all the way to Internet Explorer 5 almost 20 years ago. Unfortunately, both attempts proved unwieldy, failed to gain traction in other browsers, and ultimately were removed. While they may not be around today, their concepts formed the basis of the modern approaches in use.

JavaScript frameworks such as React, Vue or Angular follow a similar approach. One of the main reasons for their success is the ability to contain common logic in an easily shareable pattern in one form or another.

While these frameworks can improve the developer experience, they do so at a cost. Language features such as JSX need to be compiled, and many frameworks rely on a runtime to manage all of their abstractions. Wouldn't it be easier if there was a way to get the benefit without all that heavy weight? Web components allow us to do just that. 

The 4 pillars of web components

The concept of a web component consists of three APIs – Custom elements, HTML templates and the shadow DOM – with JavaScript modules gluing them together. By combining the benefits that these technologies provide, it's possible to start building custom HTML elements that look and behave just like their native counterparts.

Using a web component is much like using any other existing HTML element. They can be configured using attributes, queried for using JavaScript, and even styled through CSS. As long as the browser knows they exist, they are treated no differently.

This also allows web components to play well with other frameworks and libraries. By using the same communication mechanisms as any other element, they can be used alongside any framework that exists today or in the future.

Most importantly of all, these are all built upon web standards. The web is built on a concept of backwards compatibility, and by building a web component today, it will carry on working for years to come.

But before going any further, we should take a look at what makes up these specifications, and how you can get creating and using one today.

web components

There are four main elements that make up web components (Image credit: Matt Crouch)

01. Custom Elements

Key features:

  • Define an element's behaviour
  • React to attribute changes
  • Augment existing elements

When you hear people talk about 'web components,' quite often they are referring to the workings underneath – the custom elements API.

With this API, it is possible to create classes that power the inner workings of an element. They detail exactly what to do when an element is added, updated or removed.

class ExampleElement extends 
HTMLElement {
	static get observedAttributes()
	 { return […]; }
	attributeChangedCallback(name,
	 oldValue, newValue) {}
	connectedCallback() {}
}
customElements.define("example-element", ExampleElement);

Each custom element has a similar structure. They extend an existing HTMLElement class, which provides the groundwork for how an element should behave.

Inside, there are a few methods called reactions that are called in response to something about that element changing. For example, connectedCallback will be called when the new element appears on screen. These work similarly to the lifecycle methods found in most JavaScript frameworks.

Updating the attributes on an element can change how it behaves. When an update happens, the attributeChangedCallback reaction will fire, which details the change. This will only happen for an attribute that is defined inside the observedAttributes array.

An element needs to be defined before the browser can do anything with it. The define method here takes two arguments – the tag name, and the class it should use. All tag names must contain a - character to avoid any clashes with any future native elements.

<example-element>Content</example-element>

The element can then be written anywhere in the page as a regular HTML tag. Once a browser has an element defined, it then finds any of these matching tags and links up their behaviour to the class in a process known as 'upgrading'.

There are two types of custom element – 'autonomous' or 'customised built-in'. Autonomous custom elements, which have been covered up until now, are not related to any existing element. Much like a <div> or <span> they do not provide any meaning to their content.

A customised built-in element – as the name implies – can enhance an existing element with new functionality. They maintain that element's normal semantic behaviours, while also being open to change. If an <input> element was customised, for example, it would still be picked up and submitted as part of a form.

class CustomInput extends 
HTMLInputElement {}
customElements.define("custom-input", CustomInput, { extends: "input" });

The class of customised built-in component extends the class of the element it is customising. The definition also needs to define the tag of that element through its third argument.

<input is="custom-input" />

They are also used slightly differently. Instead of a new tag, they extend the existing tag by using the is attribute. The browser can read this, and upgrade them in the same way as it can an autonomous component.

While most browsers support autonomous custom elements, Only Chrome and Firefox support customised built-in elements. If used in a browser that does not support them, they will fall back to the regular versions of the elements they customise, which can make them safe to use.

02. HTML Templates

  • Create ready-made structures
  • Inert until called upon
  • Contains HTML, CSS and JS

Historically, browser-based templating involved stitching strings together in JavaScript, or using a library like Handlebars to parse a block special markup. More recently, the HTML templates specification defined the <template> tag, which can contain anything likely to be reused.

<template id="tweet">
	<div class="tweet">
		<span class="message"></span>
		Written by @<span 
		class="username"></span>
 </div>
</template>

On its own, it has no appearance and remains inert, meaning nothing inside is parsed or executed until told to, including requests for external media such as images or video. JavaScript cannot query the contents either, as browsers will only see it as an empty element.

const template = document.
getElementById("tweet");
const node = document.
importNode(template.content, true);
document.body.appendChild(node);

A regular query will pick up the <template> element itself. The importNode method creates a copy of its contents, with the second argument telling it to take a deep copy of everything. Finally, it can be added to the document like any other element.

Templates can contain anything an HTML page can, including CSS and JavaScript. As soon as the element is applied to the page, those styles apply and the scripts execute. Bear in mind that these will run globally and so can override styles and values if care isn't taken.

The best part about templates is that they are not just limited to web components. The examples here apply to any web page, but become particularly powerful when paired with web components, in particular the shadow DOM.

03. Shadow DOM

  • Avoid styles leaking out
  • Naming becomes simpler
  • Keep implementation logic inside

The Document Object Model – or 'DOM' – is how the browser interprets the structure of a page. By reading through the HTML, it builds up an idea on what elements in the page contain what content, and uses that to decide what to show. When using something like document.getElementById() the browser is actually looking through the DOM.

That may be okay for the layout of a page, but what about the implementation details inside an element? Pages shouldn't need to worry about what makes up the interface inside a <video> element for example. That is where the shadow DOM comes in.

<div id="shadow-root"></div>
<script>
	const host = document.
	getElementById("shadow-host");
	const shadow = host.attachShadow
	({ mode: "open"});
</script>

A shadow DOM is created when applied to an element. Any content can be added to the shadow DOM just like the regular – or 'light' – DOM, but it has no effect on what's happening outside of it. Likewise, nothing in the light DOM can access the shadow DOM directly. This means we can add classes, styles and scripts anywhere in the shadow DOM without worrying about clashes.

The best use of the shadow DOM with web components comes when coupled with a custom element. By having a shadow DOM in charge of the content, any time this component is reused, its styles and structure will not affect the rest of the page. 

04. ES and HTML Modules

  • Add in where needed
  • No build step requirement
  • Keep everything in one place

While the previous three specifications have had relatively straightforward paths to success, the packaging and reusability of them has long been a sticking point.

Originally, the HTML Imports specification defined a way for browsers to include HTML documents, much like CSS or JavaScript is already today. This would allow custom elements, along with their templates and shadow DOM, to live elsewhere and imported as needed.

Firefox chose not to implement HTML Imports in its browser, and instead favoured a newer specification around JavaScript module importing.

export class ExampleElement extends 
HTMLElement {…}
import { ExampleElement } from 
"ExampleElement.js";

Modules do not apply their content globally by default. By marking certain variables, functions or classes as exported, they can then be imported by anything that needs them and used as if they were local.

This is a great help to web components, as a custom element containing a template and a shadow DOM can be exported from one file and added to another. As long as it gets defined somewhere along the line, it is ready to use.

import { ExampleElement } from 
"ExampleElement.html";

An extension to this specification has been proposed to bring back some of the ease of HTML imports, alongside module imports. They will allow web components to be written using declarative and semantic HTML. Spearheaded by Microsoft, this feature is soon coming to Chrome and the Chromium-powered Edge browser.

Next page: How to build you own components