Web components: templates and shadow DOM
Tags: webcomponents, shadowdom, slots, templates
Published: 2022-11-30 00:31:02

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.

```html
<template id="my-button-template">
  <button>hola</button>
</template>
```

```js
const template = document.getElementById("my-button-template");
document.body.appendChild(template.content);
```

## 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.

```html
<template id="my-button-template">
  <button><slot></slot></button>
</template>
```

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:

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

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.

```html
<!-- 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>
```

```js
// my-button.js
customElements.define(
  'my-button',
  class extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.innerHTML = '<button><slot></slot></button>';
    }
  }
);
```

{% embed https://stackblitz.com/edit/web-platform-vnbcvu?embed=1&file=my-button.js&view=preview %}

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.

```html
<!-- 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>
```

```js
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);
```

{% embed https://stackblitz.com/edit/web-platform-9zostj?embed=1&file=my-button.js&view=preview %}

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.

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

# 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**!