1. Web Design
  2. HTML/CSS
  3. HTML

Extend HTML With Custom Tags and the Shadow DOM

Scroll to top
10 min read

In a previous post, I explained the basics of creating custom tags. Indeed, custom tags remove some brittleness in building great web applications. However, the quest for control does not stop, and traditional custom tags are not sufficient for building performance-rich applications. For one example, the number of style selectors in your code can grow with custom tags. This is just one of the many things that can cause performance issues.

One way of fixing this problem is through the Shadow DOM. 

The Shadow DOM works by introducing scoped styles. It does not require any special naming conventions or tools. Bundling CSS with markup becomes simple with the Shadow DOM. Also, this capability allows us to hide all details about implementation in vanilla JavaScript.

Why Use the Shadow DOM?

The Shadow DOM provides the following solutions:

  • Allows isolated elements in the DOM. Isolated elements will not be returned by queries like document.querySelector.
  • Allows scoped CSS. Scoped CSS ensures that all style rules stay within the page. It also means simpler CSS selectors, without any naming conflicts and many generic classes.

Our Example

To demonstrate the Shadow DOM, we are going to use a simple component called tuts-tabs. All references in this post will point to this piece of code. To experience the Shadow DOM, just take a look at the demo below:

Understanding the Shadow DOM

What Is a Shadow DOM?

Before you start to code with the Shadow DOM, you need to understand the regular DOM. 

HTML acts as the backbone of a website. In just a few minutes, you can create a page. When you open that page in a browser, the DOM starts to come into play. Once the browser loads a page, it starts to parse HTML into a data model. This data model is a tree structure, with nodes that represent the elements in your HTML. This data model is easy to modify and manipulate with code. 

The downside is that your entire webpage or even complex web application is treated as a single data structure. This is not very easy to debug! For example, CSS styles that are intended for one component can end up affecting another component elsewhere in your app.

When you want to isolate one part of your interface from the rest, you can use iframes. But iframes are heavy and extremely restrictive.

That's why the Shadow DOM was introduced. It is a powerful capability of modern browsers that allows web developers to include subtrees of various elements in the DOM. These subtrees of the DOM don't affect the main document tree. Technically, these are known as a shadow tree

The shadow tree has a shadow root which gets attached to a parent in the DOM. This parent is known as the shadow host

For example, if you have <input type="range"> plugged into a browser that is powered by WebKit, it will translate to a slider. Why? This is a slider because one of the subtree DOM elements understands the "range" to change its appearance and introduce slider-like functionalities. This is an advantage that the Shadow DOM brings to the tab. 

Woah, that is a lot of theory. Now, you may want to write some code to see how you can implement the Shadow DOM.

Step-by-Step Guide to Using the Shadow DOM

Step 1. Create a Shadow DOM Element 

Use element.attachShadow() to create a Shadow DOM element. 

In our example, tuts-tab, you will see this code for creating the Shadow DOM element. 

1
 let shadowRoot = this.attachShadow({mode: 'open'});

Step 2. Add Content to the Shadow Root

Next, we will add content to the shadow root using .innerHTML. Note that this is not the only way of populating your Shadow DOM. There are many APIs to help you populate the Shadow DOM. 

1
shadowRoot.innerHTML = ``

Step 3. Connect a Custom Element to the Shadow DOM

Connecting custom elements to the Shadow DOM is extremely simple. Remember, when you combine custom elements with the Shadow DOM, you will be able to create encapsulated components with CSS, JavaScript, and HTML. As a result, you will create a brand new web component that can be reused across your application. 

In our example, we create a new custom element using customElements.define(). As mentioned in the previous tutorial, the new element should have a '-' in its name. And the tuts-tabs component extends HTMLElement

As we extend HTMLElement, it is important to call super() inside the constructor. Also, the constructor is where the shadowRoot needs to be created. 

1
customElements.define('tuts-tabs', class extends HTMLElement {
2
    constructor() {
3
    super(); // always call super() first in the constructor.

4
5
    // Attach a shadow root to <tuts-tabs>.

6
    const shadowRoot = this.attachShadow({mode: 'open'});
7
    ...
8
});

Once the shadowRoot is created, you can create CSS rules for it. The CSS rules can be enclosed in the <style> tag, and these styles will only be scoped to tuts-tab.

1
customElements.define('tuts-tabs', class extends HTMLElement {
2
    constructor() {
3
    super(); 
4
    const shadowRoot = this.attachShadow({mode: 'open'});
5
    shadowRoot.innerHTML = `

6
         <!-- styles are scoped to tuts-tabs! -->

7
         <style>#tabs { ... }</style>

8
    `;
9
    }
10
    ...
11
});

Step 4. Add Styling to the Shadow DOM

CSS related to the tuts-tab can be written inside the <style></style> tags. Remember, all styles declared here will be scoped to the tuts-tab web component. Scoped CSS is a useful feature of the Shadow DOM, and it has the following properties:

  • CSS selectors don't affect components outside the Shadow DOM.
  • Elements in the Shadow DOM are not affected by selectors outside it.
  • The styles are scoped to the host element.

If you want to select the custom element inside the Shadow DOM, you can make use of the :host pseudo-class. When the :host pseudoclass is used in a normal DOM structure, it doesn't have any impact. But inside a Shadow DOM, it makes a very big difference. You will find the following :host style in the tuts-tab component. It decides the display and the font style. This is just a simple example to show how you can incorporate :host in your Shadow DOM.

One catch with :host is its specificity. If the parent page has a :host, it will be of higher specificity. All the styles inside the parent style would win. This is a way of overriding styles inside the custom element, from the outside. 

1
 :host {
2
  display: inline-block;
3
  width: 650px;
4
  font-family: 'Roboto Slab';
5
  contain: content;
6
}

As your CSS becomes simpler, the overall efficiency of the Shadow DOM improves. 

All the styles defined below are local to the shadow root. 

1
shadowRoot.innerHTML = `
2
<style>
3
:host {
4
  display: inline-block;
5
  width: 650px;
6
  font-family: 'Roboto Slab';
7
  contain: content;
8
}
9
#panels {
10
  box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
11
  background: white;
12
  border-radius: 3px;
13
  padding: 16px;
14
  height: 250px;
15
  overflow: auto;
16
}
17
#tabs slot {
18
  display: inline-flex; /* Safari bug. Treats <slot> as a parent */
19
}
20
...
21
</style>

Likewise, you have the freedom to introduce stylesheets within the Shadow DOM. When you link stylesheets inside the Shadow DOM, they will be scoped within the shadow tree. Here is a simple example to help you understand this concept.

1
shadowRoot.innerHTML = `
2
<style>
3
:host {
4
  display: inline-block;
5
  width: 650px;
6
  font-family: 'Roboto Slab';
7
  contain: content;
8
}
9
#panels {
10
  box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
11
  background: white;
12
  border-radius: 3px;
13
  padding: 16px;
14
  height: 250px;
15
  overflow: auto;
16
}
17
...
18
</style>
19
<link rel="stylesheet" href="styles.css">
20
...

Step 5. Define HTML Elements in the Custom Component

Next, we can define the HTML elements of our tuts-tab.  

In a simple tab structure, there should be titles that can be clicked and a panel that reflects the contents of the selected title. This clearly means that our custom element should have a div with titles and a div for the panel. The HTML components will be defined as below:

1
customElements.define('tuts-tabs', class extends HTMLElement {
2
    constructor() {
3
    super(); const shadowRoot = this.attachShadow({mode: 'open'});
4
    shadowRoot.innerHTML = `

5
        <style>#tabs { ... }</style>

6
        

7
        ....

8
        

9
        // Our HTML elements for tuts-tab

10
        <div id="tabs">...</div>

11
        <div id="panels">...</div>

12
        ...

13
    `;
14
    }
15
    ...
16
});

Inside the panel's div, you will come across an interesting tag called <slot>. Our next step is to learn more about slots.

Step 6. Using Slots in the Shadow DOM

Slot plays a crucial role in the Shadow DOM API. A slot acts as a placeholder inside custom components. These components can be filled by your own markup. There are three different types of slot declaration:

  1. You can have a component with zero slots. 
  2. You can create a slot with a fallback or empty content. 
  3. You can create a slot with an entire DOM tree.

In our tuts-tabs, we have one named slot for the tab titles, and another slot for the panel. The named slot creates holes that you can reference by name.

1
customElements.define('tuts-tabs', class extends HTMLElement {
2
    constructor() {
3
    super(); const shadowRoot = this.attachShadow({mode: 'open'});
4
    shadowRoot.innerHTML = `

5
        <style>#tabs { ... }</style>

6
        

7
        ....

8
        

9
        // Our HTML elements for tuts-tab

10
        <div id="tabs">

11
            <slot id="tabsSlot" name="title"></slot>

12
         </div>

13
        <div id="panels">

14
            <slot id="panelsSlot"></slot>

15
        </div>

16
        ...

17
    `;
18
    }
19
    ...
20
});

Step 7. Populate the Slots

Now, it's time to populate the slots. In our previous tutorial, we learnt about four different methods for defining custom elements, and tuts-tabs uses two of those methods for building the tab: connectedCallback and disconnectedCallback.

In connectedCallback, we will populate the slot defined in step 6. Our connectedCallback will be defined as below. We use querySelector to identify the tabsSlot and panelsSlot. Of course, this is not the only way of identifying slots in your HTML. 

Once the slots are identified, you need to assign nodes to them. In tuts-tab, we use the following tabsSlot.assignedNodes to identify the number of tabs. 

1
connectedCallback() {
2
    ...
3
    const tabsSlot = this.shadowRoot.querySelector('#tabsSlot');
4
    const panelsSlot = this.shadowRoot.querySelector('#panelsSlot');
5
6
    this.tabs = tabsSlot.assignedNodes({flatten: true});
7
    this.panels = panelsSlot.assignedNodes({flatten: true}).filter(el => {
8
      return el.nodeType === Node.ELEMENT_NODE;
9
    });
10
    ...
11
  }

Also, the connectedCallback is where we register all the event listeners. Whenever the user clicks on a tab title, the content of the panel needs to change. Event listeners for achieving this can be registered in the connectedCallback function.

Step 8. Implement the Logic and Interactivity

We are not going to drill deep into the logic of how to implement tabs and their functionality. However, remember that the following methods are implemented in our custom tuts-tab component for switching between tabs:

  1. onTitleClick: This method captures the click event on the tab titles. It helps in switching content inside the tab panel.
  2. selectTab: This function is responsible for hiding panels and showing the right panel. Also, it is responsible for highlighting the tab that was selected.
  3. findFirstSelected: This method is used to select a tab when it loads for the first time.
  4. selected: This is a getter and a setter for fetching the selected tab.

Step 9. Define Lifecycle Methods

Moving on, don't forget to define the disconnectedCallback. This is a lifecycle method in custom elements. When the custom element is destroyed from the view, this callback gets triggered. This is one of the best places to remove action listeners and reset controls in your application. However, the callback is scoped to the custom element. In our case, it would be tuts-tab.

Step 10. Use the New Component!

The final step is to use tuts-tab in our HTML. We can insert tuts-tab quite easily in the HTML markup. Here's a simple example to demonstrate its usage.

1
<tuts-tabs background>
2
  <button slot="title">Tab 1</button>
3
  <button slot="title" selected>Tab 2</button>
4
  <button slot="title">Tab 3</button>
5
  <section>content panel 1</section>
6
  <section>content panel 2</section>
7
  <section>content panel 3</section>
8
</tuts-tabs>

Conclusion

There we go! We have come to the end of an important tutorial where we created and used a custom element. The process is simple, and it proves to be extremely useful while developing webpages. I hope you try creating custom elements and share your experiences with us.

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Web Design tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.