01. November 2023

Web Components (Part 2): Browser APIs

As we’ve seen in the previous article of this blog post series, Web Components are a technology-agnostic way of building appealing web applications. In this second article, we take a look at this ready-to-use suite of web standards one-by-one and explore their features.

Frontend
Digital Transformation & Development
Software Development & Architecture
Pixabay license: https://pixabay.com/de/photos/iss-raumstation-11114/

Contents

In this text we will look at the following standards and topics:

Custom elements

With the custom elements API you can register custom HTML tags that behave like built-in HTML elements. They have attributes/properties, can emit DOM events and can be interacted with using the familiar DOM APIs (like document.querySelector etc.).

A custom element has a host element where it attaches its own DOM tree. It typically adds event listeners, implements component logic and updates its children (rendering).

Create your own custom element

To define a custom element, you can create a controller that extends HTMLElement, then register it in the global custom elements registry for a specific tag name as follows:

class MyComponent extends HTMLElement {
  constructor() {
    super(); // ⚠️ Always call the parent constructor
    // ...
  }
}

window.customElements.define(
  "my-component", // ⚠️ Must contain hyphen
  MyComponent
)

The browser now instantiates the controller for every occurrence of a tag in the markup.

Lifecycle callbacks

The controller can implement various lifecycle callbacks for certain events such as when the element has been added to the DOM or when an attribute has changed:

class MyComponent extends HTMLElement {
  connectedCallback() {} // Added to DOM
  disconnectedCallback() {} // Removed from DOM
  adoptedCallback() {} // Moved to new document
  
  // Attribute changed
  static get observedAttributes() {
    return ['x', 'y'];
  }
  attributeChangedCallback(
    name, oldValue, newValue) {}
}

Browser support

Custom elements are supported by all major browsers since the release of Chromium-based Edge 79 in January 2020, see Can I use „Custom Elements (v1)“?.

Apart from the so called autonomous custom elements we created in our example, there has been a proposal for customized built-in elements to extend existing HTML elements like . But due to technical problems the WebKit team discovered, this spec has been rejected.

Shadow DOM

Browsers have long been using shadow DOM to encapsulate built-in elements such as the

We as web developers can also use the shadow DOM API to hide the internal DOM structure and styles of a component, to avoid style clashes or leaking of styles. If you create a datepicker component for example, it should look nice no matter where it will be embedded and it should also not break the styling of the page it is embedded in. Also you want the datepicker’s internal DOM elements to be scoped, so APIs like document.querySelector won’t find them.

How does it work?

Let’s introduce some jargon. We speak about the light DOM, which is the regular DOM we see, and the shadow DOM, which is the part of the DOM that is hidden/encapsulated. You can think of it like a concert, where all you can see is the singer standing in the spotlight (light DOM), but for the whole music to be happening a band is invisibly playing in the background (shadow DOM).

Technically it works like this: a shadow root can be attached to any element in the light DOM (the shadow host). Usually the shadow host will be your custom element’s host element (e.g. ). You can then attach elements to the shadow root just as with any other DOM node. This constitutes the shadow tree which is surrounded by sort of a shadow boundary, that has special rules concerning what can pass it and how.

shadow-dom

Encapsulate your custom element’s internal DOM

Assume the component we’ve created previously renders some child elements:

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

    const paragraph = document.createElement("p");
    paragraph.textContent = "Hello World!";
    this.append(paragraph);
  }
}

In this snippet we create a new

element and attach it to the host element (the tag). Note that we are not using shadow DOM yet, the attached paragraph is fully accessible in the light DOM and as such is in the scope of the page’s styles. But we can change that easily:

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

    this.attachShadow({ mode: "open" });
    
    const paragraph = document.createElement("p");
    paragraph.textContent = "Hello World!";
    this.shadowRoot.append(paragraph);
  }
}

By calling the HTMLElement’s attachShadow function, a shadow root is attached to the host element. We then attach our paragraph to the shadow root instead of the host element itself. That’s how easy it is to encapsulate the paragraph in the component’s shadow DOM.

Note that there is also a "closed" mode setting, but it is irrelevant in practice.

How to style the shadow DOM?

So now, due to the encapsulation, any styles defined in our light DOM will not apply to the shadow tree of our component. To style the elements in the shadow tree, we have to include the styles in the shadow DOM:

class MyComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    // ...
    
    // Add style element, but you could also import a CSS file with 
    const style = document.createElement("style");
    style.textContent = `
      p {
        color: red;
      }
    `;
    this.shadowRoot.append(style);
  }
}

As we’ve already learned, the shadow DOM style encapsulation works in both directions. The style we defined in the shadow DOM for paragraphs is not affecting any of the

elements outside of the component and vice versa.

Within the shadow DOM’s CSS there are a few special selectors we can use:

/* Host element itself */
:host {}

/* Host element with "active" class */
:host(.active) {}

/* When host element has ancestor with "dark" class */
:host-context(.dark) {}

Inspection

Although the shadow DOM is encapsulated and there are restrictions on what passes the shadow boundary, it can be inspected in the browser’s Dev Tools. It is displayed as a virtual node below the host element containing the shadow tree:

shadow-dom-dev-tools

Browser support

The shadow DOM API is also supported by all major browsers since the release of Chromium-based Edge 79 in January 2020, see Can I use „Shadow DOM (v1)“?.

HTML Templates

Until now we’ve created the DOM of our component programmatically, which is a bit tedious. The HTML