Web Components

Shadow DOM

Shadow DOM API is an indispensable piece of Web componentry: it puts into effect component encapsulation - a programming technique that conceals the functional details of the component from the document. Shadow nodes of both standard and custom elements remain hidden from script routines traversing the DOM tree of a Web page.

The example below illustrating the use of Shadow DOM API creates an HTML document telling the user about Python, namely Python modules for data compression and file archiving. The page gives brief description of bz2, gzip, tarfile and zipfile. Modules summary is accompanied with four images.

The container <div> element for text and images acts as a shadow host. It will expose three subtrees: light DOM, shadow DOM, and composed DOM. Images symbolizing archive formats are placed in the light DOM:

<div id="shadow-host">
 <img src="bzip2.png">
 <img src="gzip.png">
 <img src="tar.png">
 <img src="zip.png">
</div>

The host element does not have a shadow yet, so all images are rendered and visible to the user. The shadow root is instantiated by calling the createShadowRoot() method:

var shadowHost=document.getElementById("shadow-host");
var shadowRoot=shadowHost.createShadowRoot();

Both the light DOM and shadow DOM constitute the logical DOM. Working with the logical DOM, the developer builds a shadow node tree and define insertion points for elements from the light DOM, i. e. the light DOM gets distributed into the shadow DOM.

Up to now the shadow root in our example is empty:

console.log(shadowRoot.childNodes.length); // 0

The shadowRoot object is an instance of ShadowRoot inheriting its properties and methods from DocumentFragment:

console.log(shadowRoot instanceof ShadowRoot); // true
console.log(shadowRoot.nodeType); // 11: DOCUMENT_FRAGMENT_NODE

The shadow root will get its content from the HTML template below:

<template>
 <div id="bzip2">
  <span>bz2</span> module provides a comprehensive interface . . .
 </div>
 <div id="gzip">
  <span>gzip</span> module provides a simple interface . . .
 </div>
 <div id="tar">
  <span>tarfile</span> module makes it possible to read and write tar archives . . .
 </div>
 <div id="zip">
  <span>zipfile</span> module provides API to create . . .
 </div>
</template>

The template's content is appended to the shadow root:

var template=document.querySelector("template");
shadowRoot.appendChild(template.content.cloneNode(true));

The composed DOM at this point will only have elements from the shadow DOM - four <div> elements and their child nodes. The shadow subtree is insulated from the rest of the document: for example, an attempt to apply a style rule to the shadow <span> elements will be ignored.

style sheet in the document's <head> element
<style type="text/css">
 span {
  color: gray; /* no effect for shadow <span>*/
 }
</style>

Shadow elements cannot be accessed from the document by their id attributes:

console.log(document.getElementById("bzip2")); // null

Neither does the document "see" <span> elements "lurking" in the shadow DOM:

console.log(document.getElementsByTagName("span").length); // 0

Shadow nodes are only accessible from their root:

console.log(shadowRoot.getElementById("bzip2") instanceof HTMLDivElement); // true
console.log(shadowRoot.getElementsByTagName("span").length); // 4

As a result of shadow DOM encapsulation, the innerHTML property of the shadow host and shadow root return different values:

light DOM: four <img> elements
console.log(shadowHost.innerHTML);

shadow DOM: four <div> elements with child nodes
console.log(shadowRoot.innerHTML);

An element can have several shadows: in this case the latest (or the youngest) root is applied to produce the composed DOM. The olderShadowRoot property provides access to the previous shadow:

console.log(shadowRoot.olderShadowRoot); // null: the host has a single root

A back reference from the shadow to its host is enabled by the host property:

console.log(shadowRoot.host.id); // shadow-host

So far markup of the composed Web page looks rather unimpressive: the user only sees four lines of unstyled text. To distribute images from the light DOM into the shadow DOM, the special <content> element should be brought into play. In addition, a set of CSS rules should be specified for the shadow nodes. It will require the redesign of the template:

<template>
 <style type="text/css">
  style rule for the shadow host
  :host {
   font-family: serif;
   font-size: large;
   max-width: 35%;
   text-align: justify;
  }
  style rule for <div> descendants of the shadow host
  :host div {
   border: 1px dotted silver;
   border-radius: 18px;
   box-shadow: 1px 1px 0px 1px silver;
   margin-bottom: 18px;
   padding: 6px;
  }
  style rule for inserted images
  ::content img {
   float: left;
   shape-outside: inset(0% 0% 50% 0%);
  }
</style>
 <div id="bzip2">
  <content select="img[src='bzip2.png']"></content>
  <span>bz2</span> module provides a comprehensive interface . . .
 </div>
 <div id="gzip">
  <content select="img[src='gzip.png']"></content>
  <span>gzip</span> module provides a simple interface . . .
 </div>
 <div id="tar">
  <content select="img[src='tar.png']"></content>
  <span>tarfile</span> module makes it possible to read and write tar archives . . .
 </div>
 <div id="zip">
  <content select="img[src='zip.png']"></content>
  <span>zipfile</span> module provides API to create . . .
 </div>
</template>

What has changed in the template? First of all, a range of <content> elements has designated four insertion points for images from the light DOM. Each <content> element has the special select attribute picking out an image that matches the value of the attribute.

Another change deals with the shadow style sheet. The :host pseudo-class is employed to indicate the shadow host. The CSS rule based on the :host div selector applies a dotted border and a box shadow to each of the <div> descendants of the host. The ::content pseudo-element specifies insertion points, so items from the light DOM are rendered as floating images, then their float area is shaped according to the inset() function.

Shadow style can be enhanced programmatically by using the styleSheets property of the shadow root:

console.log(shadowRoot.styleSheets.length); // 1

To add a rule to the shadow style sheet, APIs from the CSS Object Model can be employed:

shadowRoot.styleSheets[0].insertRule("span {color: gray}", 0);

The composed DOM rendered by the browser might look like this: