In a previous post, I explained the basics of creating custom tags. In fact, custom tags remove some of the fragility in creating great web applications. However, the quest for control does not stop. Furthermore, traditional custom tags are not enough for building performance-rich applications. For example, the number of style selectors in your code can increase with custom tags. This is just one of the many things that can cause performance issues.
One way to fix this is via Shadow DOM.
Shadow DOM works by introducing scoped styles. It requires no special naming conventions or tools. Grouping CSS with markup becomes simple with Shadow DOM. Also, this feature allows us to hide all the details about the vanilla JavaScript implementation.
Why use Shadow DOM?
Shadow DOM provides the following solutions:
- Allows isolated elements in the DOM. Orphaned items will not be returned by queries such as
document.querySelector
. - Allows scoped CSS. Scoped CSS ensures that all style rules remain within the page. It also means simpler CSS selectors, with no naming conflicts and lots of generic classes.
Our example
To demonstrate Shadow DOM, we will use a simple component called tuts-tabs
. All references in this post will point to this piece of code. To try Shadow DOM, check out the demo below:
Understanding Shadow DOM
What is a shadow DOM?
Before you start programming with Shadow DOM, you need to understand the normal DOM.
HTML serves as the backbone of a website. In 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 begins parsing the HTML into a data model. This data model is a tree structure with nodes. These nodes represent the elements in your HTML. This data model is easy to modify and manipulate with code.
The downside is that the entire web page or even the complex web application is treated as a single data structure. It is not very easy to debug! For example, CSS styles that target one component can end up affecting another component elsewhere in the app.
When you want to isolate one part of your interface from the rest, you can use iframe
S. But iframes are heavy and extremely restrictive.
That’s why Shadow DOM was introduced. It is a powerful capability of modern browsers that allows web developers to include substructures of various elements in the DOM. These DOM subtrees do not affect the main document tree. Technically, these are known as a tree of shadows.
The shadow tree has a shadow root which is linked to a parent in the DOM. This parent is known as the shadow guest.
For example, if you have it <input type="range">
connected to a browser powered by WebKit, it will result in a slider. How come? This is a slider because one of the subtree’s DOM elements includes “range” to change its appearance and introduce functionality similar to a slider. This is an advantage that Shadow DOM offers to the card.
Woah, this is a lot of theory. Now you may want to write some code to implement Shadow DOM in your piece of code.
Detailed guide to using 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 link for creating the Shadow DOM element.
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 isn’t the only way to populate your Shadow DOM. There are many APIs to help you populate the Shadow DOM.
shadowRoot.innerHTML = ``
Step 3. Link a custom element to the Shadow DOM
Linking custom elements to the shadow DOM is extremely simple. Remember, when you combine custom elements with Shadow DOM, you will be able to create components encapsulated with CSS, JavaScript, and HTML. As a result, you will create a new web component that can be reused in your application.
In our example, we create a new custom element using the 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()
within the manufacturer. Also, the constructor is where the shadowRoot is to be created.
customElements.define('tuts-tabs', class extends HTMLElement { constructor() { super(); // always call super() first in the constructor. // Attach a shadow root to <tuts-tabs>. const shadowRoot = this.attachShadow({mode: 'open'}); ... });
Once the shadowRoot
is created, you can create CSS
rules for it. CSS rules can be enclosed in the file <style>
tags and these styles will only have scope tuts-tab
.
customElements.define('tuts-tabs', class extends HTMLElement { constructor() { super(); const shadowRoot = this.attachShadow({mode: 'open'}); shadowRoot.innerHTML = ` <!-- styles are scoped to tuts-tabs! --> <style>#tabs { ... }</style> `; } ... });
Step 4. Add the style to the Shadow DOM
CSS related to the tuts-tab
can be written inside the <style></style>
tag. Remember, all styles declared here will scope the tuts-tab
web component. Scoped CSS is a useful feature of Shadow DOM. And it has the following properties:
- CSS selectors do not affect components outside the shadow DOM.
- Elements in the shadow DOM are not affected by selectors outside of it.
- Styles are scoped to the host element.
If you want to select the custom element inside the shadow DOM, you can use the :host
pseudo-class. When the :host
pseudoclass is used in a normal DOM structure, it would have no impact. But, within a shadow DOM, it makes a big difference. You will find the following :host
style in tuts-tab
component. Decide on the display and font style. This is just a simple example to show you how you can embed :host
in your shadow DOM.
A grip with :host
is its specificity. If the main page has a :host
will be of higher specificity. All styles within the parent style would win. This is a way to override the styles inside the custom element, from the outside.
:host { display: inline-block; width: 650px; font-family: 'Roboto Slab'; contain: content; }
As your CSS gets simpler, the overall efficiency of the shadow DOM improves.
All styles defined below are local to the shadow root.
shadowRoot.innerHTML = ` <style> :host { display: inline-block; width: 650px; font-family: 'Roboto Slab'; contain: content; } #panels { box-shadow: 0 2px 2px rgba(0, 0, 0, .3); background: white; border-radius: 3px; padding: 16px; height: 250px; overflow: auto; } #tabs slot { display: inline-flex; /* Safari bug. Treats <slot> as a parent */ } ... </style>
Likewise, you have the freedom to introduce style sheets within the shadow DOM. When you link stylesheets within the shadow DOM, they will be found within the shadow tree. Here is a simple example to help you understand this concept.
shadowRoot.innerHTML = ` <style> :host { display: inline-block; width: 650px; font-family: 'Roboto Slab'; contain: content; } #panels { box-shadow: 0 2px 2px rgba(0, 0, 0, .3); background: white; border-radius: 3px; padding: 16px; height: 250px; overflow: auto; } ... </style> <link rel="stylesheet" href="https://code.tutsplus.com/tutorials/styles.css"> ...
Step 5. Define the HTML elements in the custom component
Next, we can define the HTML elements of ours tuts-tab
.
In a simple tabbed structure, there should be titles that can be clicked and a panel that reflects the content of the selected title. This clearly means that our custom item should have a div
with titles ea div
for the panel. The HTML components will be defined as below:
customElements.define('tuts-tabs', class extends HTMLElement { constructor() { super(); const shadowRoot = this.attachShadow({mode: 'open'}); shadowRoot.innerHTML = ` <style>#tabs { ... }</style> .... // Our HTML elements for tuts-tab <div id="tabs">...</div> <div id="panels">...</div> ... `; } ... });
Inside the panels 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 within custom components. These components can be filled by your markup. There are three different types of slot declarations:
- You can have a zero slot component.
- You can create a slot with spare or empty content.
- You can create a slot with an entire DOM tree.
In our tuts-tabs
we have one slot named for card titles and another slot for the panel. The named slot creates holes that can be referenced by name.
customElements.define('tuts-tabs', class extends HTMLElement { constructor() { super(); const shadowRoot = this.attachShadow({mode: 'open'}); shadowRoot.innerHTML = ` <style>#tabs { ... }</style> .... // Our HTML elements for tuts-tab <div id="tabs"> <slot id="tabsSlot" name="title"></slot> </div> <div id="panels"> <slot id="panelsSlot"></slot> </div> ... `; } ... });
Step 7: Populate the slots
Now is the time to populate the slots. In our previous tutorial, we learned about four different methods for defining custom elements. Tuts-tabs uses two of these methods to create the tab: connectedCallback
And disconnectedCallback
.
In connectedCallback
we will populate the slot defined in point 6. Ours connectedCallback
will be defined as below. We use querySelector
to identify the tabsSlot
And panelsSlot
. Of course, this isn’t the only way to identify slots in your HTML.
Once the slots have been identified, nodes must be assigned to them. In tuts-tab
we use the following tabsSlot.assignedNodes
to identify the number of cards.
connectedCallback() { ... const tabsSlot = this.shadowRoot.querySelector('#tabsSlot'); const panelsSlot = this.shadowRoot.querySelector('#panelsSlot'); this.tabs = tabsSlot.assignedNodes({flatten: true}); this.panels = panelsSlot.assignedNodes({flatten: true}).filter(el => { return el.nodeType === Node.ELEMENT_NODE; }); ... }
Even the connectedCallback
is where we will register all event listeners. Each time the user clicks on a tab title, the content of the panel must change. Event listeners to achieve this can be registered in the file connectedCallback
function.
Step 8: implement logic and interactivity
We will not delve into logic, how to implement tabs and its features. However, remember that the following methods are implemented in our costume tuts-tab
component to switch between tabs:
-
onTitleClick
: This method captures the click event on tab titles. It helps to change the content inside the tabbed panel. -
selectTab
: This function is responsible for hiding the panels and showing the right panel. It is also responsible for highlighting the tab that has been selected. -
findFirstSelected
: This method is used to select a card when it is first loaded. -
selected
: This is a getter and a setter to retrieve the selected card.
Step 9. Define the life cycle methods
Going forward, don’t forget to define the disconnectedCallback
. This is a lifecycle method in custom items. When the custom item is deleted from the view, this callback is triggered. This is one of the best places to remove action listeners and reset controls in the 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 enter tuts-tab
, quite easily in HTML markup. Here is a simple example to demonstrate its use.
<tuts-tabs background> <button slot="title">Tab 1</button> <button slot="title" selected>Tab 2</button> <button slot="title">Tab 3</button> <section>content panel 1</section> <section>content panel 2</section> <section>content panel 3</section> </tuts-tabs>
Conclusion
Here we are! We have come to the end of an important tutorial where we create and use a custom element. The process is simple and proves extremely useful when developing web pages. We hope you try to create your own and share your experiences with us.