How do you create a custom element?

Custom elements are part of the Web Components standard. They allow you to extend the browser's native HTML elements or create new ones. When used well, this makes them portable and reusable in any later projects without worrying about backward compatibility or framework choices.

A picture of two custom elements with an exapanded DOM tree to the right, showing the HTML structure of the elements

At first, it might seem like a daunting task to learn all the new related concepts like CustomElementRegistry, ShadowDOM/ShadowRoot, , and CSS Custom Properties. My advice is to take it in steps and do small practical experiments along the way, to have some fun and get started.

Summary

To create a custom element you need to create a class that extends a native HTML element, and then register it with customElements.define:

  1. Create a JavaScript class that extends HTMLElement.
  2. Attach a Shadow Root with a <slot> and some styling.
  3. Register the element with customElements.define.

A practical example

class WjCard extends HTMLElement {
    #shadowRoot;

    constructor() {
        super();

        this.#shadowRoot = this.attachShadow({ mode: 'open' });
        this.#shadowRoot.innerHTML = `
            <style>
                .frame {
                    overflow: auto;
                    height: 100%;
                    box-sizing: border-box;
                    border: var(--wj-card-border, 1px solid #ccc);
                    border-radius: var(--wj-card-border-radius, 0.5em);
                    box-shadow: var(--wj-card-box-shadow, 0 0.25em 0.5em #aaa);
                    background: var(--wj-card-background, #fff);
                    padding: var(--wj-card-padding, 1em);
                }
            </style>

            <span class="frame">
                <slot></slot>
            </span>
        `;
    }
}

customElements.define('wj-card', WjCard);

There is not much more to it to get started with custom elements.

First, we create a new class that extends the native HTMLElement:

class WjCard extends HTMLElement {

Next in the constructor, we attach a ShadowRoot object to the element:

this.#shadowRoot = this.attachShadow({ mode: 'open' });

The shadow root is a local DOM element, similar to the global DOM, but hidden inside our custom element. One of the benefits is that most external CSS rules cannot reach this shadow root. Doing this guarantees that the styling of the surrounding page will not affect the styles of our card styling.

In our custom element, we store the ShadowRoot object in a private field that we call #shadowRoot. While a local variable can be used instead, I find the naming consistent with how the browser development tools render the DOM tree (see the image at the top of the article).

Next, we declare the HTML contents of the attached ShadowRoot element:

this.#shadowRoot.innerHTML = `
    <style>
        .frame {
            ...
            border: var(--wj-card-border, 1px solid #ccc);
            ...
        }
    </style>

    <span class="frame">
        <slot></slot>
    </span>
`;

In our custom element, we add a <style> tag into the shadow root, this causes a copy of the style tag for each instance, but that will be good enough for this example (lookup .adoptedStyleSheets for alternatives).

We also add a <div class="frame"> element to the shadow root to have something to style the frame on. This element will not receive styling rules from the outside as it is within the shadow root and therefore its look will stay consistent whichever project the custom element gets used in.

Notice also the use of CSS Custom Properties in the styles (var(--wj-card-border, 1px solid #ccc);). These allow us to "theme" our card in the various projects we use it in. More on this later on.

Inside the frame, we add a <slot> element that acts as a placeholder for where the external HTML content will be placed. It is even possible to have more than one slot by adding a name attribute to it (e.g. <slot name="header">). A <slot> element with no name attribute will be treated as the default one.

Finally, our custom element is registered in the CustomElementRegistry:

customElements.define('wj-card', WjCard);

This registration step lets the browser know that there now is a new element with the name <wj-card> and that its implementation is the class WjCard.

Notice that there is a wj- prefix. That is because custom elements must start with a prefix separated by a -. That guarantees that the name does not collide with native tag names. It also offers a namespace declaration stating which library the custom element is part of. For this custom element, we simply use wj-, as the blog's name is Writing JavaScript.

Use and reuse your elements!

Now this <wj-card> element, once imported, can then be reused anywhere, regardless if you use vanilla JavaScript or use frameworks with the following HTML:

  <wj-card>
    <h1>This is a card</h1>
    <p>
      Lorem ipsum dolor sit amet consectetur adipisicing
      elit. Similique dignissimos officiis repellat
      itaque natus qui magni voluptatum, illum, id, alias
      nostrum nemo. At eum ab quam unde est tenetur ullam.
    </p>
  </wj-card>

You can now start creating your design systems with larger sets of elements that all will have a consistent look and feel. Just imagine!

Theming your card with CSS Custom Properties

The shadow root in <wj-card> is now protected from styling from the outside world, perfect.

But wait! We still might want customizability for the CSS to match the theme of the host page.

We can achieve this using CSS Custom Properties declared on the host page:

html {
    --wj-card-border: 3px solid rgb(152, 164, 196);
    --wj-card-border-radius: 5px;
    --wj-card-box-shadow: 0 0.5em 1em #aaa;
    --wj-card-background: rgb(249, 249, 255);
    --wj-card-padding: 2em;
}

In this way, you can create a styling API for each of your custom elements, where you control which parts are possible to change from the outside, and which parts should remain locked away from the users.

Now go off and build your very own custom element libraries and design systems, and share them with the world!