JavaScript Shadow DOM

In this chapter, we are going to cover the main means of encapsulation: Shadow DOM. With it, a component can have its “shadow” DOM tree, which may be accidentally accessed from the core document, can obtain local style rules, and so on.

Built-in Shadow DOM

The browser applies DOM/CSS for creating complex browser controls. The DOM structure may be concealed, but it can be seen in the developer tools. For example, in Chrome, it is necessary to enable the “Show user agent shadow DOM” option in Dev tools. Regular JavaScript calls or selectors can’t bring built-in shadow DOM elements. A useful attribute pseudo, in the example above, is non-standard one. It can be used in style sublements with CSS, in the following way:

<!DOCTYPE html>
<html>
  <head>
    <title>Title of the document</title>
    <style>
      /* make the slider track green */
      input::-webkit-slider-runnable-track {
        background: green;
      }
    </style>
  </head>
  <body> 
    <input type="range">
  </body>
</html>

Shadow Tree

Any DOM element contains two types of DOM subtrees: Light Tree and Shadow tree. The first one is a regular DOM subtree, based on HTML children. All the subtrees, we have covered in the previous chapters, were of this type. The shadow tree is a concealed subtree that is not reflected in HTML. In case an element includes both of the subtrees, then the browser will render only the shadow tree.

Setting up a composition between light and shadow trees is also possible. You can use the shadow tree in Custom elements for hiding component internals and applying component-local styles.

Here is an example:

<!DOCTYPE html>
<html>
  <head>
    <title>Title of the document</title>
  </head>
  <body>
    <show-welcome name="W3Docs"></show-welcome>
  </body>
  <script>
    customElements.define('show-welcome', class extends HTMLElement {
      connectedCallback() {
        const shadow = this.attachShadow({mode: 'open'});
        shadow.innerHTML = `<h1>
          Welcome to ${this.getAttribute('name')}
        </h1>`;
      }
    });
  </script> 
  </body>
</html>

So, the first elem.attachShadow({mode: …}) call makes a shadow tree.

But, two limitations can turn up:

  1. Only one shadow root can be created per element.
  2. The elem should be either a custom element or one of the following: “article”, “aside”, “blockquote”, “body”, “div”, “footer”, “h1…h6”, “header”, “main” “nav”, “p”, “section”, or “span”. Take into consideration that the shadow tree can’t include elements like <img>.

The encapsulation level is set up by the mode option, which has the following values:

  1. "open": the shadow root is represented as elem.shadowRoot. The shadow tree of elem can be accessed by any code.
  2. "closed":elem.shadowRoot is permanently null.

So, the shadow Dom can only be accessed by the reference returned by attachShadow. The browser-native shadow trees like <input type="range"> are close, giving no way of accessing them.

Also, there exists an element with shadow root, known as “shadow tree host”. It is available as the shadow root host property, like this:

<!DOCTYPE html>
<html>
  <head>
    <title>Title of the document</title>
    <style>
      /* the document style will not apply to the shadow tree inside #elem*/
      h1 { color: red; }
    </style>
  </head>
  <body>
    <div id="elem"></div>
    <script>
      elem.attachShadow({mode: 'open'});
        // shadow tree has its own style 
      elem.shadowRoot.innerHTML = `
        <style> h1 { font-weight: bold; } </style>
        <h1>Welcome to W3Docs</h1>
      `;
      //need {mode: "open"}, otherwise elem.shadowRoot is null
      alert(elem.shadowRoot.host === elem); // true     
    </script>
  </body>
</html>

Encapsulation

There is a strong delimitation between the shadow DOM and the main document:

  1. From the light DOM, shadow DOM elements are not visible to querySelector. Particularly, Shadow DOM elements can have ids conflicting with the elements in the light DOM.
  2. Shadow DOM obtains own stylesheets. The style rules from the outer DOM will not be applied. Here is how it looks like:
<!DOCTYPE html>
<html>
  <head>
    <title>Title of the document</title>
    <style>
      /* the document style will not apply to the shadow tree inside #elem */
      h1 { color: green; }
    </style>
  </head>
  <body>
    <div id="elem"></div>
    <script>
      elem.attachShadow({mode: 'open'});
        // shadow tree has its own style 
      elem.shadowRoot.innerHTML = `
        <style> h1 { font-weight: bold; } </style>
        <h1>Welcome to W3Docs</h1>
      `;      
      // <h1> is only visible from queries inside the shadow tree 
      alert(document.querySelectorAll('h1').length); // 0
      alert(elem.shadowRoot.querySelectorAll('h1').length); // 1
    </script>
  </body>
</html>

So, the style from the document will not impact the shadow tree. However, the style coming from inside will do.

For getting the shadow tree elements, it is required to query from inside the tree.

Summary

In this chapter, we covered the shadow DOM, which is a way of creating a component-local DOM.

You can use shadowRoot = elem.attachShadow({mode: open|closed}) for creating shadow DOM for elem. Once there is mode="open", you can access it as elem.shadowRoot property. The shadowRoot can be populated with innerHTML and other methods of DOM.

The elements of shadow DOM:

  • contain own ids space.
  • they are not visible to JavaScript selectors from the core document like querySelector.
  • only the styles inside the shadow tree are used by them.

You will get more information about the shadow DOM in chapter Shadow DOM Slots, Composition.




Do you find this helpful?

Related articles