What is the Shadow DOM?

A couple of weeks ago, I wrote an article on what exactly the DOM is. To recap, the Document Object Model is a representation of an HTML document. It is used by browsers to determine what to render on the page, and by Javascript programs to modify the content, structure, or styling of the page.

For example, let’s take the following HTML document:

<!doctype html>
<html lang="en">
 <head>
   <title>My first web page</title>
  </head>
 <body>
    <h1>Hello, world!</h1>
    <p>How are you?</p>
  </body>
</html>

The above HTML document will result in the following DOM tree.

  • html
    • head
      • title
        • My first web page
    • body
      • h1
        • Hello, world!
      • p
        • How are you?

In the past few years, you may have heard of terms like “Shadow DOM” and “Virtual DOM”. These, although of course related to the original DOM, refer to very different concepts. In this article, I will cover what, exactly, the shadow DOM is and how it differs from the original DOM. In a future article, I’ll do the same for the virtual DOM.

Everything is global 👍🏾! Wait, everything is global 👎🏾

All elements and styles within an HTML document, and therefore the DOM, are in one big global scope. Any element on the page can be accessed by the document.querySelector() method, regardless of how deeply nested it is in the document or where it is placed. Similarly, CSS applied to the document can select any element, regardless of where it is.

This behaviour can be really great when we want to apply styles to the entire document. It’s incredibly useful to be able to select every single element on a page and set, for example, their box-sizing, all in a single line.

* { box-sizing: border-box }

On the other hand, there are times where an element requires complete encapsulation and we don’t want it to be affected by even global styles. A good example of this is third-party widgets, such as the “follow” button for Twitter. Here’s an example of of what that widget looks like:

Assuming you have Javascript enabled and you inspect the element, you’ll notice that the button is an <iframe> element, which loads a small document with the styled button you actually see.

Follow-button-widget-iframe

This is the only way that Twitter can ensure the intended styling of their widget will remain unaffected by any CSS in the hosting document. Although there are ways to use the cascade to try and achieve the same result, no other method will give the same guarantee that an <iframe> would, and that’s not ideal.

Shadow DOM was created to allow encapsulation and componentisation natively on the web platform without having to rely on tools like <iframe>s, which really weren’t made for this purpose.

A DOM within a DOM

You can think of the shadow DOM as a “DOM within a DOM”. It is its own isolated DOM tree with its own elements and styles, completely isolated from the original DOM.

Although only recently specified for use by web authors, the shadow DOM has been used by user agents for years to create and style complex components such as form elements. Let’s take the range input element, for example. To create one on the page, all we have to do is add the following element:

<input type="range">

That one element results in the following component:

If we dig deeper, we will see that this one <input> element is actually made up of several smaller <div> elements, controlling the track and the slider itself.

Range input shadow dom

This is achieved using the shadow DOM. The element that is exposed to the host HTML document the simple <input>, but underneath it there are elements and styles related to the component that do not form part of the DOM’s global scope.

How the shadow DOM works

To illustrate how the shadow DOM works, let’s recreate the Twitter “follow” button using the shadow DOM instead of an <iframe>.

First, we start with the shadow host. This is the regular HTML element within the original DOM that we want to attach the new shadow DOM to. For a component like the Follow button, it could also contain the fallback element that we would want displayed if Javascript was not enabled on the page or shadow DOM wasn't supported.

<span class="shadow-host">
  <a href="https://twitter.com/ireaderinokun">
     Follow @ireaderinokun
  </a>
</span>

Note that we didn’t just use the <a> element as the shadow host, because certain elements, primarily interactive elements, can’t be shadow hosts.

To attach a shadow DOM to our host, we use the attachShadow() method.

const shadowEl = document.querySelector(".shadow-host");
const shadow = shadowEl.attachShadow({mode: 'open'});

This will create an empty shadow root as a child of our shadow host. The shadow root is the start of a new shadow DOM in the way that the <html> element is the start of the original DOM. We can see our shadow root in the devtools inspector by the #shadow-root.

Empty shadow root

Although the regular HTML children are viewable in the inspector, they are no longer visible on the page as the shadow root takes over.

Next, we want to create the content to form our new shadow tree. The shadow tree is like a DOM tree, but for a shadow DOM instead of a regular DOM. To create our follow button, all we need is a new <a> element, which will be almost exactly the same as the fallback link we already have, but with an icon.

const link = document.createElement("a");
link.href = shadowEl.querySelector("a").href;
link.innerHTML = `
    <span aria-label="Twitter icon"></span> 
    ${shadowEl.querySelector("a").textContent}
`;

We add this new element to our shadow DOM the same way we add any element as a child to another, with the appendChild() method.

shadow.appendChild(link);

At this point, here's what our element looks like:

Plain text of "Follow-@ireaderinokun"

Finally, we can add some styles by creating a <style> element and appending that to the shadow root too.

const styles = document.createElement("style");
styles.textContent = `
a, span {
  vertical-align: top;
  display: inline-block;
  box-sizing: border-box;
}

a {
    height: 20px;
    padding: 1px 8px 1px 6px;
    background-color: #1b95e0;
    color: #fff;
    border-radius: 3px;
    font-weight: 500;
    font-size: 11px;
    font-family:'Helvetica Neue', Arial, sans-serif;
    line-height: 18px;
    text-decoration: none;   
}

a:hover {  background-color: #0c7abf; }

span {
    position: relative;
    top: 2px;
    width: 14px;
    height: 14px;
    margin-right: 3px;
    background: transparent 0 0 no-repeat;
    background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2072%2072%22%3E%3Cpath%20fill%3D%22none%22%20d%3D%22M0%200h72v72H0z%22%2F%3E%3Cpath%20class%3D%22icon%22%20fill%3D%22%23fff%22%20d%3D%22M68.812%2015.14c-2.348%201.04-4.87%201.744-7.52%202.06%202.704-1.62%204.78-4.186%205.757-7.243-2.53%201.5-5.33%202.592-8.314%203.176C56.35%2010.59%2052.948%209%2049.182%209c-7.23%200-13.092%205.86-13.092%2013.093%200%201.026.118%202.02.338%202.98C25.543%2024.527%2015.9%2019.318%209.44%2011.396c-1.125%201.936-1.77%204.184-1.77%206.58%200%204.543%202.312%208.552%205.824%2010.9-2.146-.07-4.165-.658-5.93-1.64-.002.056-.002.11-.002.163%200%206.345%204.513%2011.638%2010.504%2012.84-1.1.298-2.256.457-3.45.457-.845%200-1.666-.078-2.464-.23%201.667%205.2%206.5%208.985%2012.23%209.09-4.482%203.51-10.13%205.605-16.26%205.605-1.055%200-2.096-.06-3.122-.184%205.794%203.717%2012.676%205.882%2020.067%205.882%2024.083%200%2037.25-19.95%2037.25-37.25%200-.565-.013-1.133-.038-1.693%202.558-1.847%204.778-4.15%206.532-6.774z%22%2F%3E%3C%2Fsvg%3E);
}
`;

shadow.appendChild(styles);

Here's our final element:

The DOM vs the shadow DOM

In some ways, the shadow DOM is a "lite" version of the DOM. Like the DOM, it is a representation of HTML elements, used to determine what to render on the page and enables the modification of the elements. But unlike the DOM, the shadow DOM is not based on a full, standalone document. A shadow DOM, as it's name suggests, is always attached to an element within a regular DOM. Without the DOM, a shadow DOM doesn't exist.

blog comments powered by Disqus