Part of Series: Javascript Handbook
Javascript

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:

Last Updated 1620152315886

More Tips and Tricks for Javascript

Subscribe for Weekly Dev Tips

Subscribe to our weekly newsletter, to stay up to date with our latest web development and software engineering posts via email. You can opt out at any time.

Not a valid email