Your First Custom Element

So your interested in learning about Web Components and creating your own custom HTML tags? In this post we’ll explore the basic syntax and concepts to allow you to start dabbling with Custom Elements and Shadow DOM.

We’ll create a silly <my-title> custom element that simply stamps out a styled title. Not very useful, but it’ll help demonstrate a few starting concepts.

Getting Started

First we’ll create a separate JavaScript file that will contain everything about our custom element: its style rules, its markup, the ES6 class definition and finally register the custom element. In HTML files where we want to use our custom element, all we’ll have to do is to include that JavaScript file and we’ll be good to go to start using the new tag on our page.

Let’s wrap everything in an IIFE for good measure:

my-title.js

(function() {
  // the good stuff goes here
})();

Now let’s define a class for our custom element, which should extend HTMLElement:

(function() {
  class MyTitle extends HTMLElement {
    connectedCallback() {
      this.innerHTML = `
        <style>
          h1 {
            font-size: 2.5rem;
            color: hotpink;
            font-family: monospace;
            text-align: center;
            text-decoration: pink solid underline;
            text-decoration-skip: ink;
          }
        </style>
        <h1>Hello Alligator!</h1>
      `;
    }
  }

  window.customElements.define('my-title', MyTitle);
})();

Here are a few things to note:

  • Inside the ES6 class, this refers to the element instance itself.
  • The collectedCallback method is triggered once the element is added to the DOM. There’s also a disconnectedCallback method that gets called upon removing an element from the DOM that’s useful for cleaning-up things like event handlers.
  • We register (define) the custom element with customElement.define and passing-in the tag name as the first argument and then the class that defines the element as the second argument. Defining the element is what then allows us to use it in our HTML documents. Note that tag names should be at least two words, separated with an hyphen. That’s to prevent any future HTML element from colliding with custom elements.

With such a simple custom element, you could also define the class as an anonymous class directly in the call to customElements.define:

(function() {
  window.customElements.define(
    'my-title',
    class extends HTMLElement {
      connectedCallback() {
        this.innerHTML = `
          <style>
            h1 {
              font-size: 2.5rem;
              color: hotpink;
              font-family: monospace;
              text-align: center;
              text-decoration: pink solid underline;
              text-decoration-skip: ink;
            }
          </style>
          <h1>Hello Alligator!</h1>
        `;
      }
    }
  );
})();

You'll notice that everything is contained within the JavaScript and that we don't have any standalone HTML markup. That's because, perhaps unfortunately, HTML imports seem to be dead in the water, and the way forward for Web Components will be to define markup and styles in the JavaScript using ES6 string literals.

Shadow DOM

The above example is all well and good, but there’s one major problem: our styles are not scoped to our custom element. That means that now all h1 tags on our pages will be hotpink with an underline. For this component’s styles not to impact its outer world, a ad hoc solution would be to wrap our markup inside something like a div and then apply styles with a selector that targets only our wrapper:

this.innerHTML = `
  <style>
    .wrap-my-title h1 {
      font-size: 2.5rem;
      color: hotpink;
      font-family: monospace;
      text-align: center;
      text-decoration: pink solid underline;
      text-decoration-skip: ink;
    }
  </style>
  <div class="wrap-my-tile">
    <h1>Hello Alligator!</h1>
  </div>
`;

That’s not great and is not even foolproof if there’s another element with the wrap-my-title somewhere in your markup. Plus, it would be more performant and much nicer if we could use simple CSS selectors like h1. This is where Shadow DOM comes in. Shadow DOM allows us to scope our styles to our custom elements so that they don’t bleed out.

To use Shadow DOM, you attach a shadow root to the element and then define the markup for the element inside the shadow root:

(function() {
  class MyTitle extends HTMLElement {
    constructor() {
      super();

      this.attachShadow({ mode: 'open' });
      this.shadowRoot.innerHTML = `
        <style>
          h1 {
            font-size: 2.5rem;
            color: hotpink;
            font-family: monospace;
            text-align: center;
            text-decoration: pink solid underline;
            text-decoration-skip: ink;
          }
        </style>
        <h1>Hello Alligator!</h1>
      `;
    }
  }

  window.customElements.define('my-title', MyTitle);
})();

Here are a few things to note:

  • The class constructor is a good place to attach a shadow root and define its inner html.
  • When a custom element’s class has a constructor, you should always call super() in it.
  • The mode for the shadow root can be either open or closed. You’ll probably only ever use open, because otherwise you wouldn’t be able to set any innerHTML for it.

Using Shadow DOM, here’s what the markup will look like in your browser’s console:

view of Shadow DOM in the console

Alternate syntax

You could also accomplish the same result by creating a template element, setting its innerHTML and then cloning the content of the template as a new child to our shadow root:

(function() {
  const template = document.createElement('template');

  template.innerHTML = `
      <style>
        h1 {
          font-size: 2.5rem;
          color: hotpink;
          font-family: monospace;
          text-align: center;
          text-decoration: pink solid underline;
          text-decoration-skip: ink;
        }
      </style>
      <h1>Hello Alligator!</h1>
  `;

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

      this.attachShadow({ mode: 'open' });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
  }

  window.customElements.define('my-title', MyTitle);
})();

Usage

Using our custom element is as simple as adding the script file to the page and then using our element as we would any other regular HTML element. Note however that custom elements should always have a closing tag:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <script src="./my-counter.js"></script>
</head>

  <body>
    <my-title></my-title>
  </body>

</html>

Keep in mind that our element is not quite production ready. As it is now, the element will only work in a few modern browsers. You'll want to run the code through a transpiler like Babel for JavaScript features that are not supported across the board like ES6 classes or string literals and you'll want to use polyfills for custom elements and shadow DOM. We'll go over using Web Components polyfills in a separate post.

✖ Clear

🕵 Search Results

🔎 Searching...