Web Components and the Shadow DOM
📣 Sponsor
Web components will be familiar to you if you have worked at all with React. They are custom, replicable pieces of HTML, which can be referred to elsewhere in the code. Web components are their own HTML specification, so you may be surprised they can be used standalone with pure Javascript and HTML. Let's take a look at how to do that.
Imagine we have a status bar item which we reuse in multiple places. The structure of it might look like this:
<div class="status-bar">
<div class="size">
<div class="small">-</div>
<div class="big">+</div>
</div>
<div class="status-bar-progress">
<div class="progress">
<div class="progress-percentage"></div>
</div>
</div>
</div>
Imagine having to paste that into multiple places and maintain it so it always looks the same - and what if we change the status bar someday? We will need to go back to all places that status bar appears, and update those too! Web components allow you to reference this piece of HTML in multiple places, and gives it a unique tag, i.e:
<status-bar></status-bar>
How do we do that? Let's take a look at how to make your own web components, and how easy it is.
Step 1. Javascript
Yes you guessed it, if you want to make web components, you need to use Javascript. Let's take a quick look at how you might create a very simple DOM element, that we will call 'paragraph'.
class Paragraph extends HTMLElement {
constructor() {
super()
this.innerHTML = '<p>Hello</p>'
}
}
// define adds a custom element "alpha-paragraph" using the rules defined in the "Paragraph" class
customElements.define('alpha-paragraph', Paragraph);
<alpha-paragraph></alpha-paragraph>
The output of this will be a paragraph with the word 'Hello' inside of it. Web components depend on the class structure. We use constructor()
here to indicate what will happen when the tag loads.
Other functions
Along with constructor, we can add other functions to a class like connectedCallback()
which is fired when the DOM element is appended to a page, and attributeChangedCallback()
which is fired when an attribute of the element changes, giving us some flexibility:
class Paragraph extends HTMLElement {
constructor() {
super()
this.innerHTML = '<p>Hello</p>'
}
connectedCallback() {
// ...
}
attributeChangedCallback() {
// ...
}
}
customElements.define('alpha-paragraph', Paragraph);
Step 2. Expand Functionality
Now we have a way to create a simple element, attach callback events to it, and attach events to attribute changes, let's consider CSS. When making a standalone and clonable web component, we want it to appear physically the same in multiple places. For that it needs its own custom CSS. How do we add CSS to a web component?
To do that, we need to use something called the shadow DOM. These are essentially items appended to only one specific item. We can use this for web components by enabling the shadow DOM, and attaching our CSS to it.
Custom Element with specific CSS
class Paragraph extends HTMLElement {
constructor() {
super()
// Attach shadow DOM
let shadow = this.attachShadow({mode: 'open'});
// Append our Paragraph
shadow.innerHTML = '<p>Hello</p>'
// Add in our CSS
let style = document.createElement('style');
let elCss = style.textContent = `
p {
color: red;
font-size: 1.25rem;
}
`;
// Append our CSS
shadow.appendChild(style);
}
}
customElements.define('alpha-paragraph', Paragraph);
In the above example, any alpha-paragraph
element would have a paragraph which is red
and has a font size of 1.25rem
.
All paragraphs outside alpha-paragraph
will not be red, so you can style your other CSS independently.
Step 3. Combine with the template tag
Instead of hardcoding our web components into Javascript, we can use HTML tags instead. HTML has two tags we can use here, template
and slot
.
A template tag refers to the overall web component, while a slot is a small part of that template which can be altered. An example of a template tag looks like this:
<template id="alphaParagraph">
<slot name="paragraph-text"><p>Default Text</p></slot>
<p>Another, unchangeable paragraph</p>
</template>
The slot on the 2nd line refers to something we can change. In my opinion, we don't really need to use the template tag, but slots are useful. We can update our alpha-paragraph
to have a slot by changing the innerHTML:
class Paragraph extends HTMLElement {
constructor() {
super()
// Attach shadow DOM
let shadow = this.attachShadow({mode: 'open'});
// Append our Paragraph
shadow.innerHTML = `
<slot name="paragraph-text"><p>Default Text</p></slot>
<p>Another, unchangeable paragraph</p>
`;
// Add in our CSS
let style = document.createElement('style');
let elCss = style.textContent = `
p {
color: red;
font-size: 1.25rem;
}
`;
// Append our CSS
shadow.appendChild(style);
}
}
customElements.define('alpha-paragraph', Paragraph);
Then we can replace the slot with our own custom element using the 'slot' attribute in our HTML. In the below example, the whole slot is replaced with a paragraph element containing the text 'Custom Text'.
<alpha-paragraph color="blue">
<p slot="paragraph-text">
Custom Text
</p>
</alpha-paragraph>
Step 4. Attributes
Since all HTML elements can have attributes, we can access them using the getAttribute
function. As such, we can easily rewrite our code to have custom colors:
class Paragraph extends HTMLElement {
constructor() {
super()
// Attach shadow DOM
let shadow = this.attachShadow({mode: 'open'});
// Append our Paragraph
shadow.innerHTML = `
<slot name="paragraph-text"><p>Default Text</p></slot>
<p>Another, unchangeable paragraph</p>
`;
// Our custom color
let color = 'red';
if(this.getAttribute('color') !== null) {
color = this.getAttribute('color');
}
// Add in our CSS
let style = document.createElement('style');
let elCss = style.textContent = `
p {
color: ${color};
font-size: 1.25rem;
}
`;
// Append our CSS
shadow.appendChild(style);
}
}
customElements.define('alpha-paragraph', Paragraph);
<alpha-paragraph color="blue">
<p slot="paragraph-text">
Custom Text
</p>
</alpha-paragraph>
By putting it all in our Javascript, we ensure we can import this web component elsewhere easily. A demo of our final web component can be seen below:
More Tips and Tricks for Javascript
- Truthy and Falsy Values in Javascript
- Asynchronous Operations in Javascript
- Web Components and the Shadow DOM
- Web Workers Tutorial: Learn how Javascript Web Workers Work
- How to get the last element of an Array in Javascript
- The Complete Guide to JavaScript Set Type
- A Guide to Heaps, Stacks, References and Values in Javascript
- How to check if a user has scrolled to the bottom of a page with vanilla Javascript
- Javascript toLowerCase() - Convert Strings to Lowercase
- How to validate an email with Javascript