Steps To Understanding the Shadow DOM
By: Daryl Duckmanton
Tags: JavaScript, Application Development, HTML, Web Development, CSS

As we move forward in 2019, it is quite clear that front end development has become very important. The focus of development has been brought closer to the user interface.

Therefore in recent times, front end developers have been breaking up their websites into a vast array of components. This is to make it easier to develop in smaller batches. Under the agile methodology, we build components to match a feature or a collection of features.

As it currently stands, there are some issues that have caused component-based development to be tricky. One of the issues that we have to deal with is having our CSS styles apply to our entire document. No matter where we write it.

The DOM as we know it

So today in most browsers, we work with a singular DOM tree that represents the entire page that is loaded into the browser. Because we have the one tree, this means that all javascript and CSS that is written works against this entire document tree.

But what if we wish to import part of the markup as part of a reusable component, and this component should have its own styling that is isolated from the rest of our document? Here is an example.

<p class="highlight">This is a globally highlighted paragraph</p>



<div>

<p class="highlight">This is a highlighted paragraph in component one</p>

</div>



<div>

<p class="highlight">This is a highlighted paragraph in component two</p>

</div>

This produces the following output.

One global paragraph and 2 component paragraphs

As you can see above we have 3 paragraphs. Although not obvious in the output, the last 2 are inside of divs that could potentially represent the boundaries of 2 components.

What is wrong with this? Well lets slowly try a few things with CSS to see the problem. Starting with the CSS below.

.highlight {

font-size: 1.5rem;

background: #F00;

}

All paragraphs affected

So as you can see above, under normal circumstances, this CSS class selector will affect all three paragraphs, including the 2 that are inside of the div elements. As the author of one of these components, this is already a concern. The publisher of the document can already alter the styling of anything inside our component. This is not what we want.

So how do we address this with a single DOM tree? There are a number of ways, but none of them are full proof. The most common way is to provide a prefix in the class name of our component so that we can style it and its children with a higher specificity than what the author using our component might do in their CSS. See the example below.

.ld-componentOne .highlight {

font-size: 4rem;

background: #0F0;

}

Here you can see I have added an ld-componentOne class as part of the selector. This is one way of providing a namespace to ensure that the styles above are scoped to paragraphs with the class highlight inside of the first component. So the class name on the first component in the HTML will now look something like this.

<p class="highlight">This is a globally highlighted paragraph</p>



<div class="ld-componentOne">

<p class="highlight">This is a highlighted paragraph in component one</p>

</div>



<div class="ld-componentTwo">

<p class="highlight">This is a highlighted paragraph in component two</p>

</div>

Which will now produce the following.

It appears we have isolated the styles for component one. Now is this fully guaranteed to work and stop the author using our component from being able to style it? The answer is no unfortunately. What if the author using our component adds 2 more container divs like so.

<div class="container">

<div class="row">

<p class="highlight">This is a globally highlighted paragraph</p>



<div class="ld-componentOne">

<p class="highlight">This is a highlighted paragraph in component one</p>

</div>



<div class="ld-componentTwo">

<p class="highlight">This is a highlighted paragraph in component two</p>

</div>

</div>

</div>

And then changes the selector for the generic global style CSS from this.

.highlight {

font-size: 1.5rem;

background: #F00;

}

To a selector like this.

.container .row .highlight {

font-size: 1.5rem;

background: #F00;

}

You will now see this again.

We are back to where we started. Think about why this has happened. You now have a selector that has greater specificity than the one defined for the component. So in this case, the font size and background color will be overwritten by this change. Also technically if they sniff your classes and apply the same selector you have for your component, they too can overwrite your behavior.

At the end of the day, the overall problem is our component can not encapsulate it’s own markup and styles, and any user of our component can modify it if they wish to. Knowing this the World Wide Web Consortium (W3C) created a new specification to address this issue. This is known as the Shadow DOM.

The Shadow DOM

The Shadow DOM sounds mysterious, but the idea is pretty simple. It allows an author of a component to create its own DOM that is isolated from the regular DOM. This means that any javascript and CSS written against this new DOM can be completely encapsulated, and it’s effects are protected against CSS declared at a global area unless the component allows it.

Now, this is not a full document on how the Shadow DOM works, and there is more Shadow DOMs can do. But here is the basic idea. Please also keep in mind that this idea is not implemented in all browsers. As of this writing, I know that Chrome has adopted it.

The Shadow DOM Host

So as the regular DOM starts with the window object, a Shadow DOM can start with any element you choose (there are exceptions to the rule, but I won’t discuss them here, you can read more if you are interested). The element you choose to start a new Shadow DOM is referred to as the Shadow Host.

The Shadow DOM Root

Once you have chosen the element you wish to use to host your Shadow DOM, you then have to attach a Shadow DOM to it. See the snippet below.

<p class="highlight">This is a globally highlighted paragraph</p>



<div id="componentOne"></div>



<script>

const shadowRootOne =

document.getElementById('componentOne').attachShadow({mode: 'open'});

</script>

So in this script we are grabbing the div element with an id of componentOne and then we call a special method on that element called attachShadow. This method passes in an object with a property mode. This mode property takes either a value of ‘open’ or ‘closed’. For the purpose of this article just understand it should always be open.

What is returned from this method call is the Shadow Root. This is the object that represents the root node of your newly created Shadow DOM and it’s where you will append your other elements to.

Below is an example of adding a paragraph element against the Shadow Root that is hosted by the component.

const shadowDomParagraphElement = document.createElement('p');

shadowDomParagraphElement.setAttribute('class', 'highlight');

shadowDomParagraphElement.innerText =

'This is a highlighted paragraph in a Shadow DOM';



shadowRootOne.appendChild(shadowDomParagraphElement);

The code above now adds a new paragraph element with the highlight class to the Shadow Root and therefore is part of the new Shadow DOM. This paragraph element is encapsulated away from the global DOM so therefore it will not be found when performing both javascript and CSS selections. It will also not inherit any styles from the page author’s CSS, so at this point, it will have the default styles of the browser, as we have not yet defined styles for this Shadow DOM.

So with this in mind, if you were to try the following javascript.

const elements = document.querySelectorAll('.highlight');

console.log(elements.length);

You would see that 1 would be returned for the length. That is the paragraph that is not part of the Shadow DOM. Not 2 like you would if you added the paragraph element to an element in the regular DOM.

Also once again let's add that global default highlight style we had at the start of this article.

.highlight {

font-size: 1.5rem;

background: #F00;

}

If you now run all this code in the browser you will see the following.

Global styles not affecting Shadow DOM

Therefore you can now see the true power of Shadow DOMs. The Global styles are no longer affecting any elements inside of the Shadow DOM we just created for component one.

Adding Styles to the Shadow DOM

There are 2 ways to add styles to a Shadow DOM. We are just going to look at one in this post. The other method is to use the <template> element, but this is part of another topic. So the way to include the styles into the Shadow DOM is the same as adding any other element to the Shadow DOM. With a simple appendChild call on the Shadow Root. See below.

const shadowDomStyleElement = document.createElement('style');

shadowDomStyleElement.innerText = `

.highlight {

background: #0F0;

font-size: 4rem;

} `;



shadowRootOne.appendChild(shadowDomStyleElement);

If you add this below the script defined above, you will now see that once again component one is highlighted in green and bigger.

Component one isolated styles

However, this time if we try to change the styles of the component, by writing a global style that has a higher specificity. What happens? Well, let's see. If we first change our markup from this.

<p class="highlight">This is a globally highlighted paragraph</p>



<div id="componentOne"></div>

To this, keeping our script as is.

<div class="container">

<div class="row">

<p class="highlight">This is a globally highlighted paragraph</p>



<div id="componentOne"></div>

</div>

</div>

And then change our global selector for our highlight style from this

.highlight {

font-size: 1.5rem;

background: #F00;

}

To this

.container .row .highlight {

font-size: 1.5rem;

background: #F00;

}

We get the following result.

Shadow DOM unaffected

So as you can see, even though we changed our global style to have a higher specificity, it does not change the styling of our new component, and this is because the paragraph is encapsulated inside of a separate Shadow DOM. And this my friend is the true power of the Shadow DOM.

If you wish to see this problem in live action, you can also check out the following video. It’s in an Angular series, but it has nothing to do with Angular.