Web components: templates and shadow DOM
Joan Llenas Mas贸
Posted on November 30, 2022
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>
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.
<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:
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.
<!-- 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>
// my-button.js
customElements.define(
'my-button',
class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = '<button><slot></slot></button>';
}
}
);
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 <button></button>
</body>
</html>
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);
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));
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!
Posted on November 30, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.