What the shit is the Shadow DOM, anyway?

Shadow DOM: the next frontier in web development.

Or something. Or not.

Let’s get y’all up to speed. To start with:

What the shit is the DOM anyway?

It stands for Document Object Model. It’s the result of all of the HTML and Javascript that renders a webpage, and it’s what Javascript and CSS interact with.

Yikes. Let’s back up a bit.

To start, we’ll take a stupidly simple HTML file.

// index.html

<!DOCTYPE html>
<html>
    <head>
        <title>A Simple Webpage</title>
    </head>
    <body>
        <div class="full">
            <p>Hello world!</p>
            <p>Goodbye world!</p>
        </div>
        <div class="empty"></div>
    </body>
</html>

This file is rendered by your web browser to become the DOM. This is a tree of objects.

html
 | -> head
    | -> title
       | -> "A Simple Webpage"
 | -> body
    | -> div.full
      | -> p
         | -> "Hello world!"
      | -> p
         | -> "Goodbye world!"
    | -> div.empty

A Simple Webpage

“But Rowan,” you’re saying, “what’s the difference between that tree and the HTML file?”

We’re going to make a few changes.

// index.html

<!DOCTYPE html>
<html>
    <head>
        <title>A Simple Webpage</title>
        <script defer src="script.js"></script>
    </head>
    <body>
        <div class="full">
            <p>Hello world!</p>
            <p>Goodbye world!</p>
        </div>
        <div class="empty"></div>
    </body>
</html>
// script.js

document.querySelector('.full').innerHTML = '';
document.querySelector('.empty').innerHTML = '<p>surprise!</p>';

Now, the DOM looks like this:

html
 | -> head
    | -> title
       | -> "A Simple Webpage"
 | -> body
    | -> div.full
    | -> div.empty
       | -> p
          | -> "surprise!"

because the Javascript removed the content from our .full div and added text to our .empty one.

A page that says just "surprise!" Please ignore the <script> tag in there, I didn’t want to fuss with separate files.

The point of this was to illustrate: the DOM is the page as it is rendered, not as it is written in the HTML file.

The DOM is the page as it exists in the browser.

You can read more about it here.

CSS

Cascading Style Sheets — the last of the holy trinity. If we had added a stylesheet to the simple webpage we made earlier, like so:

.full {
    background-color: red;
}
p {
    color: blue;
}

the background color of “Hello world!” and “Goodbye world!” would have been red, and all of the text on the page would have been blue (regardless of which text it was).

Webpage saying "surprise!" in blue

This is important. Because we styled <p> elements to be blue, every <p> element on the page is blue (unless styled otherwise).

This can be useful! It allows for consistency among elements on the page — for example, if you style

// style.css

input[type='button'] {
    background-color: red;
    color: white;
    border-radius: 100%;
    overflow: hidden;
}

then every input with a button type on your website will have a red background, white text, and be a circle (or oval, more likely).

Cool.

But what if you want to have a section (or module) of your page that has a different type of button?

One way to achieve this is by making files like these:

// index.html

<!DOCTYPE html>
<html>
    <head>
        <title>This page is styled!</title>
        <link rel="stylesheet" src="style.css" />
    </head>
    <body>
        <input type="button" />
        <div id="module">
            <input type="button" />
        </div>
    </body>
</html>
// style.css

input[type='button'] {
    background-color: red;
    color: white;
    border-radius: 100%;
    overflow: hidden;
}
#module>input[type='button'] {
    background-color: white;
    color: red;
    border-radius: 0;
    overflow: scroll;
}

Two buttons on a page; one is a red circle. Look at those cute li’l buttons. Cute as two buttons.

This feels a little clunky, but at least it works. There are some ways you can make this better (e.g. SASS), and you could definitely be smarter (for example, if you gave every button a class, you could have completely independent styles — at the cost, of course, that you would be deciding styling in the markup.)

We don’t like style and markup mixing. Separation of concerns, and all that!

Enter Shadow DOM

Imagine if you could, instead, prevent document styles from entering a part of your document. If the input[type='button'] styling didn’t creep into the #module div.

Well, now you can manage that!1

Shadow DOM lets you create a new DOM, which exists inside the existing DOM. It’s sort of like a snowglobe, or a closed terrarium. I don’t like snowglobes, so we’ll stick with the closed terrarium example.

A spherical terrarium housing small succulents on a white computer desk Sort of like this, but without a big honkin’ hole. | Photo by Nielsen Ramon / Unsplash

A closed terrarium is (almost) entirely self-sufficient. There’s some stuff inside it, and that all makes up an ecosystem. If you put your terrarium in a rainforest, the terrarium wouldn’t change from if it was in a desert, or the arctic. The analogy falls down a little bit because temperatures fluctuate.

The point remains, though: wherever you put your little Shadow DOM, it exists independently, by its own rules.

Instead of ecosystem, that means its lexical scope, styles, everything.

When you attach a Shadow DOM to an element, you’re creating a little terrarium in your webpage.

// index.html

<!DOCTYPE html>
<html>
    <head>
        <title>This page has texture, and overhead lighting</title>
        <link rel="stylesheet" src="style.css" />
        <script defer src="script.js"></script>
    </head>
    <body>
        <input type="button" />
        <div id="terrarium"></div>
    </body>
</html>
// style.css

input[type='button'] {
    background-color: red;
    color: white;
    border-radius: 100%;
    overflow: hidden;
}
// script.js

terrarium = document.querySelector('#terrarium');

// set up the button to insert
let button = document.createElement("input");
button.type = 'button';

// attach a Shadow DOM. Ignore the mode for now.
terrarium.attachShadow({mode: 'open'})
         .appendChild(button);

Now we have two DOMs, each with their own button. If you create this on your own computer, you’ll see something like this: Two buttons on a page; one is a red circle. “Haven’t we seen this before?” Check out the inspector!

Starting from the top: you’ve got a round red button, and an unstyled basic button coming after it, without doing any fuckery in the markup and styling. That’s pretty cool on its own.

Then we’ll look at the Inspect view. You can see #shadow-root (open) inside of our #terrarium div.

There’s the root of our Shadow DOM. Everything inside it (in this case, just the <input type='button'> element) exists outside the normal DOM.2

Beyond Shadow DOM

The Thunderdome, from Mad Max There’s so much more! But not here, not today. If you want to get into the gory details, you’ll want to look at things like HTML imports3 and the :host CSS selector and really, just read that whole page.

It’s cool stuff! I’m still not convinced that it’s broadly useful, but am coming around after writing this post. I just had to get over associating Shadow DOM with CSS in JS.


  1. Well, you might have to use a polyfill (a bit of Javascript that replicates functionality that hasn’t been implemented in the user’s browser). [return]
  2. Well, sort of. You can apply styles to the Shadow DOM using outside CSS files, but it’s much more idiomatic, and useful, to do it from inside, so that you can create a fully self-contained module. [return]
  3. Well, this is extrmely poorly supported across browsers at this point. You’ll want to use a polyfill. [return]