We all have projects we’d instead not work on. The code has become unmanageable, the scope evolved, quick fixes applied on top of other fixes, and the structure collapsed under its weight of spaghetti code. Coding can be a messy business.

Projects benefit from using simple, independent modules which have a single responsibility. Modular code is encapsulated, so there’s less need to worry about the implementation. As long as you know what a module will output when given a set of inputs, you don’t necessarily need to understand how it achieved that goal.

Applying modular concepts to a single programming language is straightforward, but web development requires a diverse mix of technologies. Browsers parse HTML, CSS, and JavaScript to render the page’s content, styles, and functionality.

They don’t always mix easily because:

  • Related code can be split between three or more files, and
  • Global styles and JavaScript objects can interfere with each other in unexpected ways.

These problems are in addition to those encountered by language runtimes, frameworks, databases, and other dependencies used on the server.

What Are Web Components?

A Web Component is a way to create an encapsulated, single-responsibility code block that can be reused on any page.

Consider the HTML tag. Given a URL, a viewer can use controls such as play, pause, move back, move forward, and adjust the volume.

Styling and functionality are provided, although you can make modifications using various attributes and JavaScript API calls. Any number of elements can be placed inside other tags, and they won’t conflict.

What if you require your own custom functionality? For example, an element showing the number of words on the page? There’s no HTML tag (yet).

Frameworks such as React and Vue.js allow developers to create web components where the content, styling, and functionality can be defined in a single JavaScript file. These solve many complex programming problems but bear in mind that:

  • You must learn how to use that framework and update your code as it evolves.
  • A component written for one framework is rarely compatible with another.
  • Frameworks rise and wane in popularity. You’ll become dependent on the whims and priorities of the development team and users.
  • Standard Web Components can add browser functionality, which is difficult to achieve in JavaScript alone (such as the Shadow DOM).

Fortunately, popular concepts introduced in libraries and frameworks usually make their way into web standards. It’s taken some time, but Web Components have arrived.

A Brief History of Web Components

Following many vendor-specific false starts, the concept of standard Web Components was first introduced by Alex Russell at the Fronteers Conference in 2011. Google’s Polymer library (a polyfill based on the current proposals) arrived two years later, but early implementations did not appear in Chrome and Safari until 2016.

Browser vendors took time to negotiate the details, but Web Components were added to Firefox in 2018 and Edge in 2020 (when Microsoft switched to the Chromium engine).

Understandably, few developers have been willing or able to adopt Web Components, but we have finally reached a good level of browser support with stable APIs. Not everything is perfect, but they’re an increasingly viable alternative to framework-based components.

Even if you’re not willing to dump your favorite just yet, Web Components are compatible with every framework, and the APIs will be supported for years to come.

Repositories of pre-built Web Components are available for everyone to take a look at:

…but writing your own code is more fun!

This tutorial provides a complete introduction to Web Components written without a JavaScript framework. You will learn what they are and how to adapt them for your web projects. You’ll need some knowledge of HTML5, CSS, and JavaScript.

Getting Started With Web Components

Web Components are custom HTML elements such as . The name must contain a dash to never clash with elements officially supported in the HTML specification.

You must define an ES2015 class to control the element. It can be named anything, but HelloWorld is common practice. It must extend the HTMLElement interface, which represents the default properties and methods of every HTML element.

Note: Firefox allows you to extend specific HTML elements such as HTMLParagraphElement, HTMLImageElement, or HTMLButtonElement. This is not supported in other browsers and does not allow you to create a Shadow DOM.

To do anything useful, the class requires a method named connectedCallback() which is invoked when the element is added to a document:

class HelloWorld extends HTMLElement {

  // connect component
  connectedCallback() {
    this.textContent = 'Hello World!';


In this example, the element’s text is set to “Hello World.”

The class must be registered with the CustomElementRegistry to define it as a handler for a specific element:

customElements.define( 'hello-world', HelloWorld );

The browser now associates the element with your HelloWorld class when your JavaScript is loaded (e.g. ).

You now have a custom element!

CodePen demonstration

This component can be styled in CSS like any other element:

hello-world {
  font-weight: bold;
  color: red;

Adding Attributes

This component isn’t beneficial since the same text is output regardless. Like any other element, we can add HTML attributes:

This could override the text so “Hello Craig!” is displayed. To achieve this, you can add a constructor() function to the HelloWorld class, which is run when each object is created. It must:

  1. call the super() method to initialize the parent HTMLElement, and
  2. make other initializations. In this case, we’ll define a name property that is set to a default of “World”:
class HelloWorld extends HTMLElement {

  constructor() {
    this.name = 'World';

  // more code...

Your component only cares about the name attribute. A static observedAttributes() property should return an array of properties to observe:

// component attributes
static get observedAttributes() {
  return ['name'];

An attributeChangedCallback() method is called when an attribute is defined in the HTML or changed using JavaScript. It’s passed the property name, old value, and new value:

// attribute change
attributeChangedCallback(property, oldValue, newValue) {

  if (oldValue === newValue) return;
  this[ property ] = newValue;


In this example, only the name property would ever be updated, but you could add additional properties as necessary.

Finally, you need to tweak the message in the connectedCallback() method:

// connect component
connectedCallback() {

  this.textContent = `Hello ${ this.name }!`;


CodePen demonstration

Lifecycle Methods

The browser automatically calls six methods throughout the lifecycle of the Web Component state. The full list is provided here, although you have already seen the first four in the examples above:


It’s called when the component is first initialized. It must call super() and can set any defaults or perform other pre-rendering processes.

static observedAttributes()

Returns an array of attributes that the browser will observe.

attributeChangedCallback(propertyName, oldValue, newValue)

Called whenever an observed attribute is changed. Those defined in HTML are passed immediately, but JavaScript can modify them:

document.querySelector('hello-world').setAttribute('name', 'Everyone');

The method may need to trigger a re-render when this occurs.


This function is called when the Web Component is appended to a Document Object Model. It should run any required rendering.


It’s called when the Web Component is removed from a Document Object Model. This may be useful if you need to clean up, such as removing stored state or aborting Ajax requests.


This function is called when a Web Component is moved from one document to another. You may find a use for this, although I’ve struggled to think of any cases!

How Web Components Interact With Other Elements

Web Components offer some unique functionality you won’t find in JavaScript frameworks.

The Shadow DOM

While the Web Component we’ve built above works, it’s not immune to outside interference, and CSS or JavaScript could modify it. Similarly, the styles you define for your component could leak out and affect others.

The Shadow DOM solves this encapsulation problem by attaching a separated DOM to the Web Component with:

const shadow = this.attachShadow({ mode: 'closed' });

The mode can either be:

  1. “open” — JavaScript in the outer page can access the Shadow DOM (using Element.shadowRoot), or
  2. “closed” — the Shadow DOM can only be accessed within the Web Component.

The Shadow DOM can be manipulated like any other DOM element:

Sign Up For the Newsletter

connectedCallback() {

  const shadow = this.attachShadow({ mode: 'closed' });

  shadow.innerHTML = `


Hello ${ this.name }!

`; }

The component now renders the “Hello” text inside a

element and styles it. It cannot be modified by JavaScript or CSS outside the component, although some styles such as the font and color are inherited from the page because they were not explicitly defined.

CodePen demonstration

The styles scoped to this Web Component cannot affect other paragraphs on the page or even other components.

Note that the CSS :host selector can style the outer element from within the Web Component:

:host {
  transform: rotate(180deg);

You can also set styles to be applied when the element uses a specific class, e.g. :

:host(.rotate90) {
  transform: rotate(90deg);

HTML Templates

Defining HTML inside a script can become impractical for more complex Web Components. A template allows you to define a chunk of HTML in your page that your Web Component can use. This has several benefits:

  1. You can tweak HTML code without having to rewrite strings inside your JavaScript.
  2. Components can be customized without having to create separate JavaScript classes for each type.
  3. It’s easier to define HTML in HTML — and it can be modified on the server or client before the component renders.

Templates are defined in a