DEV Community

Cover image for Web components: templates and shadow DOM
Joan Llenas Masó
Joan Llenas Masó

Posted on

Web components: templates and shadow DOM

The web Components v1 specification consists of three main technologies that can be used to create reusable custom elements:

  • Custom elements
  • HTML templates
  • Shadow DOM

HTML templates

The concept of HTML templates is relatively straightforward. With the <template> tag, you create a portion of DOM content that is parsed only once. This element is not rendered in the DOM until cloned, which can be done as often as needed.

<template id="my-button-template">
  <button>hola</button>
</template>
Enter fullscreen mode Exit fullscreen mode
const template = document.getElementById("my-button-template");
document.body.appendChild(template.content);
Enter fullscreen mode Exit fullscreen mode

Content projection with <slot>

Another cool thing about the <template> tag is that you can define specific areas (the slots) for projecting the content from outside the template.

For instance, to make the previous example more useful, we could expose the button label to the outside world by adding a slot tag instead of the hardcoded value.

<template id="my-button-template">
  <button><slot></slot></button>
</template>
Enter fullscreen mode Exit fullscreen mode

This is only useful in custom elements, as we will see shortly.

Shadow DOM

The Shadow DOM is an API that gives us access to arguably the most important aspect of web components: component isolation.

The shadow DOM enables a few characteristics of the web components spec, which, I would say, drives a significant part of the remaining spec.

Enabling shadow DOM support in our component is a one-liner:

class MyWebComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' }); // shadow DOM enabled!
    this.shadowRoot.innerHTML = '<p>hola</p>';
  }
}
Enter fullscreen mode Exit fullscreen mode

The attachShadow() method creates the shadow DOM, and the shadowRoot property references the root of that shadow DOM tree.

As stated previously, an interesting effect of enabling the shadow DOM is that the DOM subtree is effectively hidden from the rest of the page.
Let's see an example.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <script type="module" src="my-button.js"></script>
    <style>
      button {
        color: red;
      }
    </style>
  </head>
  <body>
    <my-button>my-button</my-button>
    <button>Plain HTML button</button>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode
// my-button.js
customElements.define(
  'my-button',
  class extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.innerHTML = '<button><slot></slot></button>';
    }
  }
);
Enter fullscreen mode Exit fullscreen mode

We can see that our component's button is not being affected by the global button styles.

Putting it all together

Following the <my-button> example of the previous article in the series, let's extract its markup to a <template> and add shadow DOM support.

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <script src="my-button.js"></script>
  </head>

  <body>
    <template id="my-button-template">
      <style>
      button {
        cursor: pointer;
        font-size: 20px;
        font-weight: 700;
        padding: 12px;
        min-width: 180px;
        border-radius: 12px;
      }
      button.primary {
        background-color: #0b66fa;
        color: #fff;
        border: 0;
      }
      button.secondary {
        border: 1px solid rgba(0, 0, 0, 0.12);
        background-color: #fff;
        color: #000000de;
      }
      </style>
      <button><slot></slot></button>
    </template>

    <my-button>Default</my-button>
    <my-button variant="primary">Primary</my-button>
    <my-button variant="secondary">Secondary</my-button>
    <button class="primary">Plain HTML &lt;button&gt;</button>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode
class MyWebComponent extends HTMLElement {
  static get observedAttributes() {
    return ['variant'];
  }

  constructor() {
    super();
    const template = document.getElementById('my-button-template').content;
    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.appendChild(template.cloneNode(true));
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    this.render();
  }

  render() {
    const variant = this.getAttribute('variant') || '';
    this.shadowRoot.querySelector('button').className = variant;
  }
}

customElements.define('my-button', MyWebComponent);
Enter fullscreen mode Exit fullscreen mode

As we can see here, the plain HTML button is not affected by the styles in the template because they are local to the <my-button>'s shadow DOM tree.

Nothing new related to web components, but it's worth mentioning how we use templates. We have to get the reference, clone it and append it to the shadow DOM root.

const template = document.getElementById('my-button-template').content;
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(template.cloneNode(true));
Enter fullscreen mode Exit fullscreen mode

Coming up next

The following article will introduce CSS variables for theming our web components. Yes, it turns out that there are mechanisms to make styles pierce the shadow DOM!

Top comments (0)