Custom elements are one of three main technologies included in the Web Components Browser specifications (the other two are Shadow DOM and Templates). However, something that I have run across again and again is that the Web Components Browser specifications are mistaken as a one-to-one drop in replacement for component based JavaScript frameworks like React, Vue, and Svelte.

The JavaScript specification that I find to be most misunderstood is the custom elements specification.

If we are used to developing frontend components using a framework like React, we would want to be able to write a template for a function. Here is an example of a React component for rendering a Link in the UI:

import React from "react";

class AwesomeLink extends React.Component {
  render() {
    return <a class="awesome-link" href={this.props.href}>{this.props.content}</a>
  }
}

export default AwesomeLink;
React component definition for a custom link component

Once I’ve defined my component definition once, I can use it everywhere inside of my application.

import React from "react";
import AwesomeLink from "../components/awesome-link";

class App extends React.Component {
  render() {
    return <div id="app">
      <AwesomeLink href="https://example.com" content="My Awesome Link" />
    </div>;
  }
}
Using React link component within a React App

I then expect that at runtime, my app will render the link with my specified properties and I will get valid HTML:

<div id="app">
  <a class="awesome-link" href="https://example.com">My Awesome Link</a>
</div>
Expected HTML from React App component

Now we come to custom elements.

The first time we look at a custom element, it does look like it could be a drop-in replacement for a JavaScript component framework.

The Custom elements specification allows us to define new HTML Elements to which we can add custom behavior with JavaScript. Since this is a browser specification, it works out of the box and does not need any extra JavaScript dependencies for browsers which support the API (all modern browsers do support custom elements).

class AwesomeLink extends HTMLElement {
  connectedCallback() {
    // JavaScript callback which is run as soon as the browser has loaded the element

  }
}

customElements.define("awesome-link", AwesomeLink);
JavaScript code to define a custom element using JavaScript

Once we’ve defined our custom element, we can then use it within our pure HTML file and, if JavaScript is activated for the page, as soon as the Browser loads the <awesome-link> element, it will call our custom JavaScript callback and we can do all kinds of powerful things.

Because of our background with component based JavaScript frameworks, the first thing we may want to do is build a self-contained component which would transform itself into some awesome HTML at runtime. We would want to write some HTML like the following:

<body>
  <awesome-link href="https://example.org">My Awesome Link</awesome-link>
</body>
HTML including a custom element which we want to transform itself at runtime

Once a browser triggers our connectedCallback() method, we expect that our HTML would appear as follows:

<body>
  <awesome-link>
    <a class="awesome-link" href="https://example.org">My Awesome Link</a>
  </awesome-link>
</body>
What we expect our HTML to look like once `connectedCallback` has run

Is it possible to build something like this using custom elements without any other JavaScript libraries?

Yes. It would look something like the following code:

class AwesomeLink extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<a class="awesome-link" href="${this.getAttribute("href")}">${this.textContent}</a>`;
  }
}
Writing a custom element to perform templating at runtime.

But notice here that the actual templating for the HTML for the component is not done in the custom element itself (e.g. the AwesomeLink JavaScript class) but instead uses JavaScript template literals to create the HTML template.

This is because the custom element specification was never intended to be used for templating!

In fact, that is probably the reason that there is a whole other part of the Web Components specification which is specifically for Templates.

By combining both custom elements and templates, we could build a component which performs templating at runtime.

<body>
  <awesome-link href="https://example.org">My Awesome Link</awesome-link>
  <template id="awesomeLinkTemplate">
    <a class="awesome-link" href><slot name="content"></slot></a>
  </template>
</body>
HTML page including a custom element and a template definition
class AwesomeLink extends HTMLElement {
  connectedCallback() {
    let template = document.getElementById("awesomeTemplateLink").content.querySelector("*").cloneNode(true);
    template.href = this.getAttribute("href");
    template.querySelector("slot[name=content]").outerHTML = this.textContent;
    
    this.innerHTML = "";
    this.appendChild(template);
  }
}

customElements.define("awesome-link", AwesomeLink);
JavaScript for performing templating in a custom element using HTML templates

I’ve used both of these approaches for lightweight templating within custom elements, although I personally prefer template strings because they feel less unwieldy than HTML templates for this use case.

With both of these approaches, it is also absolutely crucial to be vigorous about HTML escaping of the HTML before it ever reaches the browser because we otherwise open ourselves up for XSS attacks. Ideally, HTML escaping is something that we would like our templating engine to take care of for us, which shows the limitations of the templating possibilities that we have available with Vanilla JavaScript. For components that require a lot of client-side templating, it is probably worthwhile to use a templating library or framework.

What custom elements are actually good at

I’ve now discussed in detail what custom elements are NOT good at, and now I want to look at where they excel.

The main benefit of custom elements is that they allow us to hook our JavaScript behavior directly into the lifecycle of the HTML DOM. Consider this JavaScript snippet which I took from this article on transclusion:

$('a.replace-link').each(function() {
  var link = $(this);
  var content = $('<div></div>').load(link.attr('href'), function() {
    link.replaceWith(content);
  });
});
Initializing a transclusion component using jQuery

This initialization function for the JavaScript component is executed under the assumption that the HTML code which it is targeting (in this instance, an anchor (a) tag with the class replace-link) has already been loaded. If that is not the case, then the component will not be initialized correctly. Additionally, if HTML code containing the component is added to the page after the initialization function has been executed, these components will also not be initialized correctly. Our example component replaces a link element on a page with the content of the page that the link points to – if that content also includes an instance of the component within the page, the component will not be initialized unless we remember to call the initialization function after loading the HTML.

In my opinion, this is the main problem that custom elements solve. We can define the initialization logic for our component within the connectedCallback method inside of our custom element class. Then the browser takes care of the initialization: as soon as the element appears in the DOM, the browser knows exactly which JavaScript function needs to be called.

class ReplaceLink extends HTMLElement {
    connectedCallback() {
        let link = this.querySelector("a");
        fetch(link.href)
            .then(response => response.text())
            .then(html => this.outerHTML = html);
    }
}

customElements.define("replace-link", ReplaceLink);
Initializing a component within the connectedCallback of a custom element class

In this example, we can also see the second major benefit of using custom elements: because a custom element extends HTMLElement, all of the JavaScript API methods for an HTMLElement will be bound to the this variable within the class. This means that I can use this.querySelector("a") to retrieve a link element which is contained within the element in the DOM or any of the other useful properties and methods available for an HTMLElement (e.g. getAttribute, classList, dispatchEvent, etc.).

Look at how we can use our new replace-link custom element in our HTML:

<replace-link>
 <a href="https://example.org">My Awesome Link</a>
</replace-link>
Using a custom element in an HTML document

If JavaScript doesn’t load for some reason, what do we have? We have a fully functional link that the user can click on to view the content! Only if and when JavaScript is loaded will the JavaScript function be activated to replace the link with its contents.

This is the power of progressive enhancement. If the JavaScript fails, the page still works. The user may not even notice that something went “wrong”, especially when decent CSS styles for the link are provided. I have written many progressively enhanced web components using custom elements, and every time I am absolutely delighted with the result.