On this page

Shadow DOM

Web components became a real solution for creating reusable HTML custom elements when the shadow DOM was created. The shadow DOM is used to isolate all the elements inside your web component from the outside document. It is a separate island, free to behave however it wants, without needing anything from outside, free from breaking something else on the page that happens to have the same id value or class name.

In a document (the web page) you have a tree like structure of elements. The root of the tree is known as the document DOM. You can create a web component that hosts its own DOM that is isolated from the rest of the document. The web component is a normal element, but it hosts a shadow DOM. Inside the shadow DOM you can add other elements, create events, and do almost everything you can do in the document DOM.

Each web component can have its own shadow DOM. The same web component, used twice on a page, can create elements in their shadow DOM, that do not interferer with each other or anything else in the document. In the above diagram the web component creates an element with an id. The document DOM cannot have duplicate id values, but each shadow DOM has their own id with the same value, but this is perfectly fine.

How it Works

Creating a web component with a shadow DOM is very simple. Take a look at the example code below.

class ShadowDomExample extends HTMLElement {
  constructor() {
    super();

    // Attach shadow DOM root
    this.attachShadow({ mode: 'open' });

    // Set inner HTML
    this.shadowRoot.innerHTML =
      '<p><b>Shadom DOM Example</b></p><p>I exist in my own DOM.</p>';
  }
}

The best place to attach the shadow DOM is in the constructor function. To create and attach a shadow DOM you need to call the element's attachShadow function. This function takes a single object parameter which controls how the shadow DOM is created and processed. For now, we just set the mode value to 'open'. The function does return the shadow root object but it also sets the element's shadowRoot member. This is the root element of the shadow DOM, which is where we can add all our own internal elements to.

<!DOCTYPE html>
<html lang="en">
  <head>...</head>
  <body>
    <p>...</p>
    <shadow-dom-example>
      #shadow-dom (open)
        <p>
          <b>Shadow DOM Example</b>
        </p>
        <p>I exist in my own DOM.</p>
    </shadow-dom-example>
  </body>
</html>  

You will notice that the first sub-element in the web component is the #shadow-root and that it is labelled as open. Below this are the elements we added to the shadow DOM.

Element Isolation

One of the benefits of using a shadow DOM is element isolation. All the elements inside one has no affect on the elements in another. This is best seen when using an element id. A document (web page) must only contain unique id values. Calling the function document.getElementById could return the wrong element if the id value was being used by multiple elements. We can, however, use the same id value within a shadow DOM, even if the web component it belongs to is used multiple times on the web page. The id value still needs to be unique within a single shadow DOM. The code below is used to setup our example.

constructor() {
  super();

  // Attach shadow DOM root
  this.attachShadow({ mode: 'open' });

  // Set inner HTML
  this.shadowRoot.innerHTML =
    '<p id="result">Number = ?</p><button id="button">Change Text</button>';

  // Get result element
  this._resultElement = this.shadowRoot.getElementById('result');

  // Get button element
  this._buttonElement = this.shadowRoot.getElementById('button');
}  

We create and attach the shadow DOM. We then set the inner HTML elements, which contains a button and a paragraph for the result output. Pressing the button will create and show a random number.

There are two copies of the web component, and therefore the same id is used in each one, but they are isolated and do now affect each other. Pressing the button that looks for the id="result", using the document.getElementById function, will show that they can not find the elements with that id value, because it is isolated within the shadow DOM.

We do call a similar function, this.shadowRoot.getElementById, but this looks for the element within the shadow DOM, not the document.

Open and Closed

You can create and attach a shadow DOM as either open or closed, but what is the difference? It is all linked to the host element's shadowRoot property. If the shadow DOM is open, then this property contains the shadow root object, but if the shadow DOM is closed then the property is set to null, and therefore cannot be used. But if it cannot be used then how do add your own elements inside?

The attachShadow function not only creates and attaches the shadow DOM, but it also returns the shadow root object. With this you can insert your own elements. Take a look at the example code below.

class ClosedShadowDom extends HTMLElement {
  constructor() {
    super();

    // Attach shadow DOM root (returns the shadow root object)
    const shadowRoot = this.attachShadow({ mode: 'closed' });

    // Because the shadow DOM is closed the shadowRoot property is always null
    // therefore we need to use the returned shadowRoot variable instead

    // Set inner HTML
    shadowRoot.innerHTML = '<div>Closed shadow DOM</div>';
  }
}  

Once the constructor has finished, there is no access to the shadow root object. One of the benefits to this is that outside JavaScript code cannot access any inner parts of our web component, they cannot access the internal elements of the shadow DOM. Lets see an example of this.

There are two web components, one using an open shadow DOM and the other using a closed one. You can check to see if you have access to the shadowRoot property for each one.

Even though the closed shadow DOM does not allow you to go inside the shadow DOM, you can see inside using the browsers debug element viewer. You can also make adjustments to styles and other parts.

<!DOCTYPE html>
<html lang="en">
  <head>...</head>
  <body>
    <p>...</p>
    <open-shadow-dom id="open">
      #shadow-root (open)
        <div>Open shadow DOM</div>
    </open-shadow-dom>
    <closed-shadow-dom id="closed">
      #shadow-root (closed)
        <div>Closed shadow DOM</div>
    </closed-shadow-dom>
  </body>
</html>  

Because of all this, having a closed shadow DOM does not seem to be of any real use, and it may be best to always create an open one.

Unsupported Elements

Most standard elements do not allow a shadow DOM to be attached. Any autonomous web component can, but if you are creating one derived from something other than HTMLElement then it is unlikely you will be able to. There are some exceptions however. Below is a list of the tags you can do this with.

Tag Class Name
<article> HTMLElement
<aside> HTMLElement
<blockquote> HTMLElement
<body> HTMLBodyElement
<div> HTMLDivElement
<footer> HTMLElement
<h1> to <h6> HTMLHeadingElement
<header> HTMLElement
<main> HTMLElement
<nav> HTMLElement
<p> HTMLParagraphElement
<section> HTMLElement
<span> HTMLSpanElement

Lets take a look at how this works and how it does not work.

The first web component, derived from the paragraph element, is able to attach its shadow DOM without any problems. The other one, derived from the anchor element, fails when it tries to attach a shadow DOM, giving you the following error message.

Uncaught DOMException: Failed to execute 'attachShadow' on 'Element': This element does not support attachShadow