Shadow DOM: The Basics
In my previous post, I introduced the Shadow DOM and made the case for why it is important. Today, we’ll get our hands dirty with some code! By the end of this post, you’ll be able to create your own encapsulated widgets that pull in content from the outside world and rearrange their bits and pieces to produce something wholly different.
Let’s get started!
Support
In order to try the examples, I suggest you use Google Chrome, v33 or greater.
Codez!
I’ve created a sketchbook for this post and future Web Components related stuff. You can grab the sketchbook on GitHub. For each of the examples that I cover, I’ll link to the sketch so you can quickly try things out.
A Basic Example
Sketch 0: basic
Let’s take a look at a very simple HTML document.
In HTML, every element is considered to be a node. When you have a group of these nodes nested inside one another, it is known as a node tree.
What’s unique about shadow DOM is that it allows us to create our own node trees, known as shadow trees, that encapsulate their contents and render only what they choose. This means we can inject text, rearrange content, add styles, etc. Here’s an example:
Using the code above, we’ve replaced the text content of our .widget
div using a shadow tree. To create a shadow tree, we first specify that a node should act as a shadow host. In this case, we use .widget
as our shadow host. Then we add a new node to our shadow host, known as a shadow root. The shadow root acts as the first node in your shadow tree and all other nodes descend from it.
If you were to inspect this element in the Chrome Dev Tools, it would look something like this:
Do you see how #shadow-root
is grayed out? That’s the shadow root we just created. The takeaway is that the content inside of the shadow host is not rendered. Instead, the content inside of the shadow root is what gets rendered.
This is why, when we run our example, we see Im inside yr div!
instead of Hello, world!
We can visualize this process in another graph:
Let’s do one more example, to help it all sink in.
A Basic Example (cont.)
Sketch 1: basic-cont
I’m taking our previous example and adding two additional elements to it. I’ve done this to illustrate that working with the shadow DOM is really not so different from working with the regular DOM. You still create elements and use appendChild
or insertBefore
to add them to a parent. In the Chrome Dev Tools it looks like this:
Just like before, the content in the shadow host, Hello, world!
, is not rendered. Instead the content in the shadow root is rendered.
“That’s easy enough,” you might say. “But what if I actually want the content in my shadow host to render?”
Well, dear reader, you’ll be happy to know that’s not only possible but it’s actually one of the killer features of shadow DOM. Let’s keep going and I’ll show you how!
Content
Sketch 2: content
In our last two examples, we’ve completely replaced the content in our shadow hosts with whatever was inside our shadow root. While this is a neat trick, in practice, it’s not very useful. What would be really great is if we could take the content from our shadow host and then use the structure of the shadow root for presentation. Separating the content from the presentation like this allows us to be much more flexible with how our page actually renders.
First things first, to use the content in our shadow host we’re going to need to employ the new <content>
tag. Here’s an example:
If you’ve been following along, this should look familiar. Using the <content>
tag, we’ve created an insertion point that projects the text from the .pokemon
div, so it appears within our shadow <h1>
. Insertion points are very powerful because they allow us to change the order in which things render without physically altering the source. It also means that we can be selective about what gets rendered.
You may have noticed that we’re using a template tag instead of building our shadow DOM entirely in JavaScript. I’ve found that using <template>
tags makes the process of working with the shadow DOM much easier.
Let’s look at a more advanced example to demonstrate how to work with multiple insertion points.
Selects
Sketch 3: selects
In this example, we’re building a very simple biography widget. Because each definition field needs specific content, we have to tell the <content>
tag to be selective about where things are inserted. To do this, we use the select
attribute. The select
attribute uses CSS selectors to pick out which items it wishes to display.
For instance, <content select=".last-name">
looks inside the shadow host for any element with a matching class of .last-name
. If it finds a match, it will render its content inside of the shadow DOM.
Changing Order
As I mentioned before, insertion points allow us to change the rendering order of our presentation without needing to modify the structure of our content. Remember, the content is what lives in the shadow host and the presentation is what lives in the shadow root/shadow DOM. A good example would be to swap the rendering order of the first and last names.
By simply changing the structure of our template
, we’ve altered the presentation, but the order of the content remains the same. To understand what I mean, take a look at the Chrome Dev Tools.
As you can see, .first-name
is still the first child in the shadow host, but we’ve made it appear as if it comes after .last-name
. We did this by changing the order of our insertion points. That’s pretty powerful when you think about it.
Greedy Insertion Points
You may have noticed, at the end of the .bio-template
, we have a <content>
tag with an empty string inside of its select
attribute.
This is considered a wildcard selection and it will grab any content in the shadow host that is left over. The following three selections are all equivalent:
As an experiment, let’s move this wildcard selection to the top of our template
.
You’ll notice that it completely changes the presentation of our widget by moving all content inside of the <p>
tag. That’s because selections are greedy and items may only be selected one time. Since we have a wildcard selection at the top of our template, it grabs all of the content from the shadow host and doesn’t leave anything for the other selects
.
Dominic Cooney (@coonsta) does a great job of describing this in his post on Shadow DOM 101. In it, he compares the selection process to party invitations.
The content element is the invitation that lets content from the document into the backstage Shadow DOM rendering party. These invitations are delivered in order; who gets an invitation depends on to whom it is addressed (that is, the select attribute.) Content, once invited, always accepts the invitation (who wouldn’t?!) and off it goes. If a subsequent invitation is sent to that address again, well, nobody is home, and it doesn’t come to your party.
Mastering selects and insertion points can be pretty tricky, so Eric Bidelman (@ebidel) created an insertion point visualizer to help illustrate the concepts.
He also created this handy video explaining it :)
Conclusion
We still have a lot more to talk about, but for today, let’s wrap things up. Tomorrow, we’ll dig into CSS style encapsulation and later JavaScript and user interaction. As always, if you have questions hit me up on twitter or leave a comment. Thanks!