Welcome back to The Ultimate Guide to Web Components blog series! We’re going to discuss the state of Web Components, help unravel best practices, reveal hidden tips and tricks and make sure that you get a real grasp on the workings of Web Components.
In this article we’ll be focusing on Shadow DOM, answering questions such as “What is Shadow DOM?” and “Why use Shadow DOM?”. Excited? Let’s get going!
Articles in this series:
- The Ultimate Guide to Web Components
- Lifecycle Hooks in Web Components
- Understanding Shadow DOM in Web Components (this post!)
Today’s blog is all about the coolest sounding API of all: Shadow DOM!
After reading this post, you’ll be primed with new knowledge on things like:
- A deep understanding and view of Shadow DOM
- Benefits and why use Shadow DOM
- Some real-world patterns you can use today
Before we can start learning about Shadow DOM we first have to know a little bit more about the inner workings of a web “page”, or document. Let’s investigate the DOM and its workings, doing so will enable us to further understand and learn Shadow DOM.
Table of contents
What is the DOM?
The base of a web page consists of HTML, a markup language for humans to write and understand. HTML gives us a way to add structure and content, but machines do need a little bit more than that. That’s why the Document Object Model (aka the DOM) is created. The DOM then has an API that can be used by our applications to manipulate document’s structure, content and styling.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
When a browser loads a web page, the HTML code is converted to a data model that consists of “Objects” and “Nodes”. In addition, a “DOM Tree” is generated in which the structure of the webpage is stored. You can think of the DOM as a JavaScript representation of our HTML structure.
With some example code, a DOM Tree could look like this:
html
└─ head
│ └─ title
│ └─ Web Components ftw!
└─ body
└─ h1
└─ Let's learn about Shadow DOM!
The HTML that generates this tree looks like this:
<html>
<head>
<title>Web Components ftw!</title>
</head>
<body>
<h1>Let's learn about Shadow DOM!</h1>
</body>
</html>
The Objects
and Nodes
created by the DOM contain properties and methods that, in short, can be called by developers like us to manipulate a web page. This makes the DOM, in contrast to static HTML, a dynamic element of the web that offers enormous flexibility to both applications and developers!
Combining the JavaScript language with the DOM, we can have full programming capabilities whilst being able to talk to our web pages.
All elements and styles in HTML (and therefore in the DOM as well) are part of one giant, global scope that makes it possible to reach every element with some utilities the DOM exposes. It’s very likely you’ve seen document.querySelector
before, and if you - you’ve already used the DOM!
The same applies to CSS Styles. It’s possible to access (and affect) every element with a single CSS Selector, is that a good thing? Not necessarily!
The case for encapsulation
In today’s world full of component-driven architecture there is still the case that CSS globally applied could create issues across the rest of our code. This is due to the way that CSS was created and how it works. It’s not necessarily a bad thing, but it’s good to know that in the component architecture world we could make better use of styling through the idea of encapsulation. This will help us prevent any unwanted side effects whilst keeping our component code clean, minimal and easy to extend and refactor.
Before the introduction of Shadow DOM, it’s typical that developers used an <iframe>
that loads a seperate page into an application. The <iframe>
technique is not exactly clean, minimal or easily extendable though. However, the page is not affected by the application code and it’s shielded from the rest of the application so that you don’t have to worry about behavioral or styling leaks.
Besides the fact iframes do give us this separation - there are serious security issues we open ourselves up to by using them. They aren’t secure (malicious plugins can be injected), they’re not very accessible and jeopardize Search Engine Optimization (SEO). Surely there’s a better way? Enter the Shadow DOM.
If you’re new to Shadow DOM, you’re going to love this technology.
What is Shadow DOM?
The Shadow DOM is just like any other DOM that browsers can generate from HTML code, but the difference is the way they are being generated - and how they are used and behave in relation to other elements on a web page.
Normally, the DOM is being built by generating nodes and appending them as a child to an element, which composes the DOM Tree.
Take a look at the following example:
const button = document.createElement('button'); // create a brand new button element
button.innerHTML = 'Click me'; // give that poor button a label
document.querySelector('body').appendChild(button); // append the button to the document's body
The DOM Tree will then look like this:
html
└─ head
│ └─ title
│ └─ Web Components ftw!
└─ body
└─ h1
└─ Let's learn about Shadow DOM!
└─ button
└─ Click me
So where does Shadow DOM enter the picture?
Shadow DOM isn’t some secret thing that we need to enable or install, it actually is generated by the browser for you in a completely isolated DOM Tree called the Shadow Tree.
The Shadow Tree comes complete with its own elements and styling that is added to an element as a child. The element to which the tree is added is called the Shadow Host and the root is called (no surprise here …) Shadow Root. The hidden nature is where Shadow DOM takes its name from. “Why is it hidden?” I hear you ask.
Does this mean that the Shadow DOM cannot be reached from outside at all? Nope!
The Shadow DOM can still emit events that can be captured by the rest of an application. The shadowRoot
can also be accessed if the mode is set to open (see below momentarily for the code snippet that demonstrates this).
There’s lots of new terminology here, and it’s definitely not an easy subject to grasp. So let’s dig a little deeper to uncover more by learning from example.
Creating Shadow DOM Elements
Attaching a new element as part of a Shadow Tree to a host element is very easy by using the Element.attachShadow()
method.
Let’s see this in action:
// acts as Shadow Host
const div = document.createElement('div');
// the Shadow DOM's content
const header = document.createElement('h1');
header.innerHTML = 'Here lies some Shadow DOM';
// attach the shadow root to the shadow host
// and set the mode to 'open'
const shadowRoot = div.attachShadow({ mode: 'open' });
// attach heading to the Shadow DOM
shadowRoot.appendChild(header);
This results in the following HTML structure:
<div>
#shadow-root (open)
<h1>Here lies some Shadow DOM</h1>
</div>
Our DOM Tree would then look like this:
document
└─ shadow host
└─ shadow root
└─ h1
└─ Here lies some Shadow DOM
Element.attachShadow()
knows two modes, open
and closed
, which specifies the encapsulation mode. Open means that shadow root is accessible from JavaScript outside the shadow root. Closed denies access from outside. The difference between the two modes:
// open mode
element.shadowRoot; // returns a ShadowRoot object
// closed mode
element.shadowRoot; // returns null
Shadow DOM and Elements
You can attach a shadow root to almost all elements, but there are some exceptions. This is because of security reasons (i.e. <a>
) or that it doesn’t make sense (i.e. <img>
). The elements you can attach shadow root to are:
<article>
<aside>
<blockquote>
<body>
<div>
<footer>
<h1>
<h2>
<h3>
<h4>
<h5>
<h6>
<header>
<main>
<nav>
<p>
<section>
<span>
And, of course, you can attach it to any Custom Element with a valid name (see the “How to create a Web Component” section).
Another thing to consider are events. An event’s target is adjusted to stay encapsulated and make to look like if it originated from the component itself, rather than from one of its children. And not all events are bubbled out of the component as well! The following events do bubble outside of Shadow DOM:
- Composition Events
- Drag Events
- Focus Events
- Input Event
- Keyboard Events
- Mouse Events
- Wheel Events
Read more on event bubbling and capturing.
Shadow DOM and Web Components
To guarantee the encapsulating properties of the Custom Elements web standard, Shadow DOM is included in the list of APIs that form the Web Components Standard.
As we just have learned, a shadow root can be appendend to any component in the DOM Tree, but how is it linked to a Custom Element? The answer is simple: To itself! Just look at the following example:
export class MyAwesomeComponent extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<!-- this styling is scoped to the element! -->
<style>h1 { color: red; }</style>
<h1>Greetings from the dark side of the DOM</h1>
`;
}
}
customElements.define('my-awesome-component', MyAwesomeComponent);
The exact same happens as in the earlier example where we attached Shadow DOM to a ‘regular’ DOM Element. The only difference is that the Custom Element generates its own instance of Shadow DOM when the component is initialized. This happens in the constructor
.
Another interesting aspect of Shadow DOM is that styling is automatically scoped to the component, preventing it from leaking to the rest of the application. Encapsulation for the win!
This results in the following HTML structure:
<my-awesome-component>
#shadow-root (open)
<style>h1 { color: red; }</style>
<h1>Greetings from the dark side of the DOM</h1>
</my-awesome-component>
The document’s DOM Tree looks like this:
document
└─ my-awesome-component
└─ shadow root
└─ style
└─ color: red
└─ h1
└─ Greetings from the dark side of the DOM
By default, you can’t see
#shadow-root
in your browser’s developer tools. To enable it, visit the Settings cog, then inside Preferences you’ll find “Show user agent shadow DOM” - hit the checkbox and you’ll be able to see any Shadow DOM out in the wild!
Browser support
Evergreen browsers (Chrome, Firefox and Safari) are currently supporting Shadow DOM V1 and Edge is currently working on support. In the meantime there’s a set of polyfills available that simulate the missing browser functionalities and which allow you to use Web Components in all evergreen browsers and even Internet Explorer 11.
I’ve included a screenshot below from WebComponents.org that shows the current browser support – a really nice community guide worth checking out and adding to your Bookmarks:
Summary
Shadow DOM is a technique that ensures that code, styling and structure are not encapsulated in a separate, hidden DOM Tree
which makes it perfectly compatible with Web Components. Shadow DOM can be attached to almost every element and is supported by almost every browser. It’s also the coolest sounding API we have available!
Want to learn more? Come and check out some of our JavaScript and framework related courses over here!