DEV Community

ndesmic
ndesmic

Posted on

Web Components, an Overview

Web Components are a selection of technologies that together can be used to bring us a lot of the benefits of custom component and rendering libraries but natively on the web. Currently, there are 3 main pillars:

  • HTML Templates
  • Shadow DOM
  • Custom Element

HTML Templates are a way to add inert HTML to the DOM that you can clone. Shadow DOM adds encapsulation for styles and DOM so that they are treated as internal to an element. Custom Elements are a way to register custom tags that can have user-defined behavior.

Templates

Templates are fairly self-explanatory, first you write the HTML:

<template id="foo">
  <div>Hello World</div>
</template>
Enter fullscreen mode Exit fullscreen mode

Then you can use it like this:

const div = document.getElementById("foo").content.cloneNode(true);
document.body.appendChild(div);
Enter fullscreen mode Exit fullscreen mode

The template HTML is not rendered but it does get parsed into DOM nodes. It stays inert and you can use the .content property to get a reference to it's inner content as a documentFragment. Typically, this is combined with cloneNode(true) (with the true indicating a deep subtree copy) so you can reuse the DOM nodes without the overhead of having to parse HTML from strings.

Custom Elements

Custom elements allow you to register tag names that can generate custom HTML elements. Note that these are specifically called "custom elements" but it's really easy to get confused or habitually call them "web components" even though that's not accurate.

Here's perhaps the simplest of custom elements:

customElements.define("my-element", class extends HTMLElement {
  connectedCallback(){
    this.innerHTML = `<div>Hello World!</div>`
  }
});
Enter fullscreen mode Exit fullscreen mode

Assuming this script is registered with the DOM, we can use it like this:

<my-element></my-element>
Enter fullscreen mode Exit fullscreen mode

Custom elements are classes that a registered with customElement.define. Note that the class must extend HTMLElement. Some browsers will let you extend other elements for better accessibility but Safari unfortunately does not allow this. They can optionally have methods for a few lifecycle methods`

  • connectedCallback when the element is attached to the DOM
  • adoptedCallback when the element is moved to a new DOM (you likely won't use this one)
  • disconnectedCallback when the element is removed from the DOM
  • attributeChangedCallback when the elements "observed" (more on this later) attributes change.

connectedCallback is probably the most interesting as that's what's going to kick off all of your code. One special thing to note is that tag names must contain a hyphen. This is primitive namespacing mechanism so HTML can continue to add new non-hyphenated elements without breaking existing code.

Shadow DOM

Shadow DOM is perhaps the most complicated pillar as it fundamentally changes how DOM works by creating a separate DOM tree for elements that has very limited interaction with the main DOM.

Using the example above, here's how we would produce it:

`

customElements.define("my-shadow-element", class {
  connectedCallback(){
    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = "<div id='x'>Hello From the Shadow World!</div>";
  }
});
Enter fullscreen mode Exit fullscreen mode

(excuse the extra tick, it's a markdown parsing bug)

attachShadow adds a shadow root to the element and there can be only one. If we were to run querySelector("#x") on the document, the inner div would not be returned as it does not exist in the light DOM. It is still possible to query it from the element directly querySelector("my-shadow-element").shadowRoot.querySelector("#x") though. However, even this can be disabled using attachShadow({mode: "closed"}) which will cause .shadowRoot to always return null. Under these circumstances only the reference returned from attachShadow can be used to access it, and if you throw it away nothing can access that shadow DOM anymore.

So why would you want this? Shadow DOM is used for DOM encapsulation, you can have guarantees that other scripts cannot add to or modify the DOM as they don't have references. Shadow DOM also fully encapsulates CSS. Outer CSS cannot get in, except in highly specific ways, and styles used inside the element cannot get out. This allows you to build components where the internals can be be kept internal, the same way if you use a <video> element the controls to play, stop, change volume and navigate are not directly accessible to outside code, they're just part of the element itself. Shadow DOM can also define slots in which child content can be added from the outside to specific locations. This is done by using the slot tag:

customElements.define("my-shadow-element", class {
  connectedCallback(){
    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = `
     <div id='x'>Hello From the Shadow World!</div>
     <slot name="content"></slot>`;
  }
});
Enter fullscreen mode Exit fullscreen mode

Outside element are matched to slots by name so:

<my-shadow-element>
  <div slot="content">Outside</div>
</my-shadow-element>
Enter fullscreen mode Exit fullscreen mode

Will be slotted into the content slot. There is ongoing work to make this work via index as well.

HTML Imports?

If you look at earlier specs and articles you might find a thing called HTML imports. This was a proposed way into which to import other HTML content (scripts, templates, styles) from other HTML documents via a link tag. It was a very interesting idea that never really took off. Since this was going on at the same time ESM was being spec'd out, it was hard to align and eventually dropped. Now the expectation is that we'll use imports for everything (and if you use some build tools you already do!). This doesn't exist in browsers yet though. This will be cool for JS but it's a bit sad there is no plan for HTML to get similar powers.

Hopefully that gives a brief overview of the landscape. It's not too complicated but work is ongoing and new features are added constantly to each pillar.

Practical Application

While this is just a 10,000 foot view of the landscape, if you are looking for more practical application, including API design and how-tos of real world components, or just some inspiration I've started a series on building components completely from scratch, just JS, HTML and CSS: https://dev.to/ndesmic/series/9830

Oldest comments (0)