In this codelab, you will learn about the basics of Web Components and how they work.

Web Components are a set of low-level browser features that allow us to write modular, encapsulated and reusable HTML elements. Web Components are based on web standards and work in any environment that supports basic HTML and JavaScript. This means that there is no complex setup required for you to get started.

Web Components align with the way that browsers have always worked, they are pretty low level and straightforward. For most projects you will still want to use libraries or frameworks. But instead of each framework developing their own component model, they can use the features that are already baked into the browser.

Web components are quite flexible and have a multitude of possible use cases. The more prominent use case is to build reusable UI components. This is especially powerful, for the reason that UI components can be reused in applications that are built with different technologies.

Furthermore, Web Components can also be used to compose entire applications and are also a perfect fit for static/server-rendered pages where the components just add interactivity after the initial render.

What you need

What you'll learn

Browsers are moving pretty fast, and new features and APIs are being added all the time. In this codelab you will learn about the three main browser features that Web Components consist of:

How it works

This codelab will take you through Web Components step by step, explaining each section as you go along. At the bottom of each section, there is a "View final result" button. This will show you the correct code that you should end up with, in case you get stuck. The steps are sequential, thus results from the previous steps carry over to the next step.

You can follow this codelab using anything that is able to display a simple HTML page. We recommend using an online code editor like jsbin, but you can also create your own HTML page using your favorite IDE.

To get started, let's create a basic HTML page:

<!DOCTYPE html>

<html>
  <body>
    <h1>Hello world!</h1>
  </body>
</html>

If you run this in the browser and see hello world, you're good to go!

First, we will take a look at the most important Web Component feature: Custom Elements.

Modify your HTML to wrap the "Hello world" message in an element called <cool-heading>:

<cool-heading>
  <h1>Hello world!</h1>
</cool-heading>

Currently, your browser does not recognize the <cool-heading> tag. When the browser encounters an unknown HTML tag like <cool-heading>, it will just render it as an inline element and move on. With the custom elements API, we can tell the browser what to do with the HTML tag that we have just created.

We need to do this in javascript, so let's add a script tag to the bottom of our <body> element:

<script>
  // your code will go here
</script>

To create a custom element we need to declare a class that extends the HTMLElement class. This is the base class that powers all other native elements such as the <input> and <button> elements. Now, let's go ahead and create a new class for our <cool-heading> element:

class CoolHeading extends HTMLElement {
  connectedCallback() {
    console.log('cool heading connected!');
  }
}

After creating our class we can associate it with a tagname by defining it in the custom elements registry. This way, whenever the browser's parser gets to the <cool-heading> tag, it will instantiate and apply our class to that specific element:

customElements.define('cool-heading', CoolHeading);
View final result
<!DOCTYPE html>

<html>
  <body>
    <cool-heading>
      <h1>Hello world!</h1>
    </cool-heading>

    <script>
      class CoolHeading extends HTMLElement {
        connectedCallback() {
          console.log('cool heading connected!');
        }
      }

      customElements.define('cool-heading', CoolHeading);
    </script>
  </body>
</html>

In the previous step, you learned how to set up a basic custom element. Now it's time to actually make it do something useful.

When the browser instantiates our custom element it triggers some lifecycle callbacks. For now, the only lifecycle methods that we are going to learn about are connectedCallback() and disconnectedCallback() as seen below:

class MyElement extends HTMLElement {
  constructor() {
    super();
    // called when the class is instantiated (standard js)
  }

  connectedCallback() {
    /**
     * called when the element is connected to the page
     * this can be called multiple times during the element's lifecycle
     * for example when using drag&drop to move elements around
     */
  }

  disconnectedCallback() {
    // called when the element is disconnected from the page
  }
}

Because our element extends from the HTMLElement class, when it gets instantiated, the class instance is an actual live DOM element. All the methods and properties we are familiar with from a regular DOM element exist here as well.

For example, let's add some styles to our element:

class CoolHeading extends HTMLElement {
  connectedCallback() {
    this.style.color = 'blue';
  }
}

The text in our element should now appear blue.

To respond to user input, we can add an event listener to our element or one of its children. Let's add one that will change the color of the element when clicked:

class CoolHeading extends HTMLElement {
  constructor() {
    super();

    this.addEventListener('click', () => {
      this.style.color = 'red';
    });
  }

  connectedCallback() {
    this.style.color = 'blue';
  }
}

If we run this code in the browser, the element should turn red when clicked on.

View final result
<!DOCTYPE html>

<html>
  <body>
    <cool-heading>
      <h1>Hello world!</h1>
    </cool-heading>

    <script>
      class CoolHeading extends HTMLElement {
        constructor() {
          super();

          this.addEventListener('click', () => {
            this.style.color = 'red';
          });
        }

        connectedCallback() {
          this.style.color = 'blue';
        }
      }

      customElements.define('cool-heading', CoolHeading);
    </script>
  </body>
</html>

The second Web Components feature that we will look into is HTML templates. When writing Web Components, we usually need to do more than just setting some styles or text. We often need to render larger pieces of HTML as part of our component and update parts of it when the user interacts with it.

To do this efficiently, the browser provides us with a <template> element. This element allows us to define the structure of a piece of a HTML upfront, and efficiently clone it when needed. This is a lot faster than recreating the same HTML structure each time. Using and cloning templates is (intentionally) pretty low level. You can read more about the basic API here.

Let's create a <template> inside the body of our previous excercise.

<template></template>

Next, we'll move the inner HTML of <cool-heading> to the template:

<template>
  <h1>Hello world!</h1>
</template>

Notice that "Hello World" is no longer rendered on the page. This is because the content of a <template> elements is inert. Nothing inside it is displayed, images don't get downloaded and scripts don't run.

Template example

In the connectedCallback() function we can now retrieve the template and use it. We clone its content by doing a deep import and add the cloned element to the children of the component.

connectedCallback() {
  const template = document.querySelector('template');
  const clone = document.importNode(template.content, true);
  this.appendChild(clone);
}

You should see the template rendered on the screen.

In order to avoid writing this boiler plate code over and over again, you can use libraries to do the heavy lifting. lit-element is a base element that will allow you to easily use templates and much more. We will use this library in a follow-up codelab.

View final result
<!DOCTYPE html>

<html>
  <body>
    <template>
      <h1>Hello world!</h1>
    </template>

    <cool-heading></cool-heading>

    <script>
      class CoolHeading extends HTMLElement {
        constructor() {
          super();

          this.addEventListener('click', () => {
            this.style.color = 'red';
          });
        }

        connectedCallback() {
          const template = document.querySelector('template');
          const clone = document.importNode(template.content, true);
          this.appendChild(clone);
        }
      }

      customElements.define('cool-heading', CoolHeading);
    </script>
  </body>
</html>

The last important Web Component feature we will look into is Shadow DOM. Traditionally, the context of HTML and CSS have always been global. This scales pretty badly, because we constantly need to make sure that the id's of all the elements are unique and often CSS selectors can get pretty complex. This is why many front-end frameworks offer some form of encapsulation. Web Components provide us with this capability using a "Shadow DOM", this capability is now built into the browser. When adding child elements to a Shadow DOM of a component, they will not be direct children of our element, but rather they are wrapped inside of a shadow root.

This shadow root is a special type of DOM node which encapsulates the elements inside of it. Styles defined inside this shadow root do not leak out, and styles defined outside the shadow root do not reach in, hence encapsulation. Also, it's not possible to use a regular querySelector() to select elements inside or outside the shadow root. This way we can build reusable components and gives us the confidence that they will always work the same way, no matter the environment.

Let's try this out by attaching a shadow root to our component, making it available as a property called shadowRoot.

connectedCallback() {
  const template = document.querySelector('template');
  const clone = document.importNode(template.content, true);
  this.attachShadow({ mode: 'open' });
  this.appendChild(clone);
}

Now, instead of adding the cloned content to the direct children, we add the content to the shadow root's children.

connectedCallback() {
  const template = document.querySelector('template');
  const clone = document.importNode(template.content, true);
  this.attachShadow({ mode: 'open' });
  this.shadowRoot.appendChild(clone);
}

To test the encapsulation of our component, let's add some styles to the <template> element:

<template>
  <style>
    h1 {
      color: red;
    }
  </style>
  <h1>Hello world!</h1>
</template>

When we refresh the page, our element should now be styled.

To see the encapsulation in action, we can add the same content of our template to the page outside our component:

<!DOCTYPE html>

<html>
  <body>
    <template> ... </template>

    <h1>Hello world!</h1>

    <cool-heading></cool-heading>

    <script type="module">
      ...
    </script>
  </body>
</html>

If we refresh the page again, we should see that the styles inside our component do not affect the HTML outside of it.

Shadow DOM example

Similarly we can add styles to the main page, and you will see that it doesn't affect the HTML inside our component:

<!DOCTYPE html>

<html>
  <body>
    <template> ... </template>

    <style>
      h1 {
        color: pink;
      }
    </style>

    <h1>Hello world!</h1>

    <cool-heading></cool-heading>

    <script type="module">
      ...
    </script>
  </body>
</html>

Not all CSS properties are encapsulated in this way. Inherited properties such as fonts and color do inherit through the shadow root when they are applied to a parent element.

For example we can change the front of our page and it will affect the text inside our component as well:

<style>
  body {
    font-family: monospace;
  }

  h1 {
    color: pink;
  }
</style>
View final result
<!DOCTYPE html>

<html>
  <body>
    <style>
      body {
        font-family: monospace;
      }

      h1 {
        color: pink;
      }
    </style>

    <template>
      <style>
        h1 {
          color: red;
        }
      </style>
      <h1>Hello world!</h1>
    </template>

    <h1>Hello world</h1>

    <cool-heading></cool-heading>

    <script>
      class CoolHeading extends HTMLElement {
        constructor() {
          super();

          this.addEventListener('click', () => {
            this.style.color = 'red';
          });
        }

        connectedCallback() {
          const template = document.querySelector('template');
          const clone = document.importNode(template.content, true);
          this.attachShadow({ mode: 'open' });
          this.shadowRoot.appendChild(clone);
        }
      }

      customElements.define('cool-heading', CoolHeading);
    </script>
  </body>
</html>

Web Components are being used in the wild by many companies and in many projects. Some examples:

YouTube

YouTube is one of the most popular sites on the internet. Their main website is built with custom elements to split up the different parts of their page in components.

On older browsers, they load a plain old HTML site.

Example YouTube web component

Github

Github uses Web Components for various parts of their website. They're using just the Custom Elements API, relying on global styling. They use them as a progressive enhancement. On browsers without support for Custom Elements (or when javascript is turned off), there is a fallback text that is displayed.

Their elements are open source, you can find them here.

Example github web component

Example github web component

Twitter

Twitter utilizes Web Components for embedding tweets. They're using both Custom Elements and Shadow DOM, because they need to ensure the styling of the tweet is consistent across pages and the styling of the component doesn't interfere with the styling of the page.

On browsers which don't support Web Components, Twitter uses an iframe to achieve similar functionality (albeit with a much higher cost).

Example twitter web component

Example twitter web component code

Video

The <video> element is built into the browser, and it's actually also using Shadow DOM. When you put a video element on a page the component renders extra UI for the controls in the Shadow DOM.

You can inspect the Shadow DOM of these elements on most browsers after enabling a setting in your DevTools.

Example video element shadow dom

Example video element shadow dom