Hostingheaderbarlogoj
Join InMotion Hosting for $3.49/mo & get a year on Tuts+ FREE (worth $180). Start today.
Advertisement

Intro to Shadow DOM

by
Gift

Want a free year on Tuts+ (worth $180)? Start an InMotion Hosting plan for $3.49/mo.

Take any modern web page and you will notice that it invariably contains content stitched together from a variety of different sources; it may include the social sharing widgets from Twitter or Facebook or a Youtube video playing widget, it may serve a personalized advertisement from some ad-server or it may include some utility scripts or styles from a third party library hosted over CDN and so on. And if everything is HTML based (as is preferred these days) there is a high probability of collisions between the markup, scripts or styles served from various sources. Generally, namespaces are employed to prevent these collisions which solve the problem to some extent, but they don't offer Encapsulation.

Encapsulation is one of the pillars on which the Object Oriented Programming paradigm was founded and is normally used to restrict the internal representation of an object from the outside world.

Coming back to our problem, we can surely encapsulate the JavaScript code using closures or using the module pattern but can we do the same for our HTML markup? Imagine that we have to build a UI widget, can we hide the implementation details of our widget from the JavaScript and CSS code that is included on the page, which consumes our widget? Alternatively, can we prevent the consuming code from messing up our widget's functionality or look and feel?


Shadow DOM to the Rescue

The only existing solution that creates a boundary between the code you write and code that consumes, is ugly - and operates by using a bulky and restrictive iFrame, which brings with itself another set of problems. So are we forced to adapt to this approach always?

Not anymore! Shadow DOM provides us an elegant way to overlay the normal DOM subtree with a special document fragment that contains another subtree of nodes, which are impregnable to scripts and styles. The interesting part is that it's not something new! Various browsers have already been using this methodology to implement native widgets like date, sliders, audio, video players, etc.

Enabling Shadow DOM

At the time of this writing, the current version of Chrome (v29) supports inspecting Shadow DOM using Chrome DevTools. Open Devtools and click on the cog button at the bottom right of the screen to open the Settings panel, scroll down a bit and you will see a checkbox for showing Shadow DOM.

Turn on Shadow DOM

Now that we have enabled our browser, lets check out the internals of the default audio player. Just type:

<audio width="300" height="32" src="http://developer.mozilla.org/@api/deki/files/2926/=AudioTest_(1).ogg" autoplay="autoplay" controls="controls">
 Your browser does not support the HTML5 Audio.
 </audio>

Into your HTML markup. It shows the following native audio player in supported browsers:

audio_player

Now go ahead and inspect the audio player widget that you just created.

Shadow DOM of Native Date Widget

Wow! It shows the internal representation of the audio player, which was otherwise hidden. As we can see, the audio element uses a document fragment to hold the internal contents of the widget and appends that to the container element ( which is known as Shadow Host ).

Shadow Host & Shadow Root

  • Shadow Host: is the DOM element which is hosting the Shadow DOM subtree or it is the DOM node which contains the Shadow Root.
  • Shadow Root: is the root of the DOM subtree containing the shadow DOM nodes. It is a special node, which creates the boundary between the normal DOM nodes and the Shadow DOM nodes. It is this boundary, which encapsulates the Shadow DOM nodes from any JavaScript or CSS code on the consuming page.
  • Shadow DOM: allows for multiple DOM subtrees to be composed into one larger tree. Following images from the W3C working draft best explains the concept of overlaying the nodes. This is how it looks before the Shadow Root's content are attached to Shadow Host element:
    Normal Document Tree & Shadow DOM Subtrees

    When rendered, the Shadow tree takes place of Shadow Host's content.

    Composition Complete

    This process of overlaying the nodes is often referred to as Composition.

  • Shadow Boundary: is denoted by the dotted line in the image above. This denotes the separation between the normal DOM world and the Shadow DOM world. The scripts from either side cannot cross this boundary and create havoc on the other side.

Hello Shadow DOM World

Enough chit-chat I say, Let's get our hands dirty by writing some code. Suppose we have the following markup, which shows a simple welcome message.

<div id="welcomeMessage">Welcome to My World</div>

Add the following JavaScript code or use this Fiddle:


var shadowHost = document.querySelector("#welcomeMessage");
var shadowRoot = shadowHost.webkitCreateShadowRoot();
shadowRoot.textContent = "Hello Shadow DOM World";

Here we create a Shadow Root using the webkitCreateShadowRoot() function, attach it to a Shadow Host and then simply change the content.

Notice the vendor-specific prefix webkit before the function name. This indicates that this functionality is currently supported on some webkit-based browsers only.

If you go ahead and run this example in a supported browser, then you would see "Hello Shadow DOM World" instead of "Welcome to My World" as the Shadow DOM nodes have over-shadowed the normal ones.

Disclaimer: As some of you may notice, we're mixing the markup with scripts, which is generally not recommended and Shadow DOM is no exception. We have deliberately avoided the use of templates so early in the game in order to avoid any confusion. Otherwise Shadow DOM does provide an elegant solution to this problem and we will get there pretty soon.


Respecting Shadow Boundary

If you try and access the content of the rendered tree using JavaScript, like so:

var shadowHost = document.querySelector("#welcomeMessage");
var shadowRoot = shadowHost.webkitCreateShadowRoot();
shadowRoot.textContent = "Hello Shadow DOM World";

console.log(shadowHost.textContent);
 // Prints "Welcome to My World" as the shadow DOM nodes are encapsulated and cannot be accessed by JavaScript

You will get the original content "Welcome to My World" and not the content which is actually rendered on the page, as the Shadow DOM tree is encapsulated from any scripts. This also means that the widget that you create using Shadow DOM is safe from any unwanted/conflicting scripts already present in the page.

Styles Encapsulation

Similarly, any CSS selector is forbidden to cross the shadow boundary. Check the following code where we have applied red color to the list items, but that style is only applied to the nodes which are part of the parent page, and the list items which are part of Shadow Root are not affected with this style.

<div class="outer">
  <div id="welcomeMessage">Welcome to My World</div>
  <div class="normalTree">Sample List
  <ul>
      <li>Item 1</li>
      <li>Item 2</li>
  </ul>
  </div>
</div>
<style>
   div.outer li {  
      color: red;  
   } 
   div.outer{  
      border: solid 1px;  padding: 1em; 
   }
</style>
<script type="text/javascript">
    var shadowHost = document.querySelector("#welcomeMessage");
    var shadowRoot = shadowHost.webkitCreateShadowRoot();
    shadowRoot.innerHTML = ["<div class='shadowChild'>",
                            "Shadow DOM offers us Encapsulation from",
                            "<ul>",
                            "<li>Scripts</li>",
                            "<li>Styles</li>",
                            "</ul>",
                            "</div>"
                            ].join(',').replace(/,/g,"");
</script>

You can see the code in action on Fiddle. This encapsulation applies even if we reverse the direction of traversal. Any styles which are defined inside the Shadow DOM does not affect the parent document and remains scoped to the Shadow Root only. Check this Fiddle for an example, where we apply the blue color to list items in Shadow DOM but the parent document's list items are unaffected.

There is however one notable exception here; Shadow DOM gives us the flexibility to style the Shadow Host, the DOM node which is holding the Shadow DOM. Ideally it lies outside the Shadow boundary and is not a part of Shadow Root, but using the @host rule, one can specify the styles that can be applied to Shadow Host as we have styled the welcome message in the example below.

<div id="welcomeMessage">Welcome to My World</div>
<script type="text/javascript">
  var shadowHost = document.querySelector("#welcomeMessage");
  var shadowRoot = shadowHost.webkitCreateShadowRoot();
  shadowRoot.innerHTML = ["<style>",
                          "@host{ ",
                             "#welcomeMessage{ ",
                                "font-size: 28px;",
                                "font-family:cursive;",
                                "font-weight:bold;",
                             "}",
                          "}",
                          "</style>",
                          "<content select=''></content>"
                          ].join(',').replace(/,/g,"");
</script>

Check this Fiddle as we style the Shadow Host's welcome message using the styles defined in Shadow DOM.

Creating Style Hooks

As a widget developer, I might want the user of my widget to be able to style certain elements. This is achievable by plugging a hole into the shadow boundary using custom pseudo elements. This is similar to how some browsers create style hooks for the developer to style some internal elements of a native widget. For example, to style the thumb and the track of the native slider you can use the ::-webkit-slider-thumb and ::webkit-slider-runnable-track as follows:


input[type=range]{
    -webkit-appearance:none;
 }
 input[type=range]::-webkit-slider-thumb {
    -webkit-appearance:none;
    height:12px;
    width:12px;
    border-radius:6px;
    background:yellow;
    position:relative;
    top:-5px;
 }
 input[type=range]::-webkit-slider-runnable-track {
    background:red;
    height:2px;
 }

Fork this Fiddle and apply your own styles to it!

Event Re-Targeting

If an event that originates from one of the nodes in Shadow DOM crosses the Shadow Boundary then it is re-targeted to refer to the Shadow Host in order to maintain encapsulation. Consider the following code:


<input id="normalText" type="text" value="Normal DOM Text Node" />
<div id="shadowHost"></div>
<input id="shadowText" type="text" value="Shadow DOM Node" />
<script type="text/javascript">
    var shadowHost = document.querySelector('#shadowHost');
    var shadowRoot = shadowHost.webkitCreateShadowRoot();
    var template = document.querySelector('template');
    shadowRoot.appendChild(template.content.cloneNode(true));
    template.remove();
    document.addEventListener('click', function(e) { 
                                 console.log(e.target.id + ' clicked!'); 
                              });
</script>

It renders two text input elements, one via Normal DOM and another via Shadow DOM and then listens for a click event on the document. Now, when the second text input is clicked, the event is originated from inside Shadow DOM and when it crosses the Shadow Boundary, the event is modified to change the target element to Shadow Host's <div> element instead of the <input> text input. We have also introduced a new <template> element here; this is conceptually similar to client-side templating solutions like Handlebars and Underscore but is not as evolved and lacks browser support. Having said that, using templates is the ideal way to write Shadow DOM rather than using script tags as has been done so far throughout this article.


Separation of Concerns

We already know that it's always a good idea to separate actual content from presentation; Shadow DOM should not embed any content, which is to be finally shown to the user. Rather, the content should always be present on the original page and not hidden inside the Shadow DOM template. When the composition occurs, this content should then be projected into appropriate insertion points defined in the Shadow DOM's template. Let's rewrite the Hello World example, keeping in mind the above separation - a live example can be found on Fiddle.

<div id="welcomeMessage">Welcome to Shadow DOM World</div>
<script type="text/javascript">
    var shadowRoot = document.querySelector("#welcomeMessage").webkitCreateShadowRoot();
    var template = document.querySelector("template");
    shadowRoot.appendChild(template.content); 
    template.remove();
</script>

When the page is rendered, the content of the Shadow Host is projected into the place where the <content> element appears. This is a very simplistic example where <content> picks up everything inside the Shadow Host during composition. But it can very well be selective in picking the content from Shadow Host using the select attribute as shown below

<div id="outer">How about some cool demo, eh ?
    <div class="cursiveButton">My Awesome Button</div>
</div>
<button>
  Fallback Content
</button>
<style>
button{ 
   font-family: cursive;  
   font-size: 24px;
   color: red; 
}
</style>
<script type="text/javascript">
    var shadowRoot = document.querySelector("#outer").webkitCreateShadowRoot(); 
    var template = document.querySelector("template"); 
    shadowRoot.appendChild(template.content.cloneNode(true));
    template.remove();
</script>

Check out the live demo and play with it to better understand the concept of insertion points and projections.


Web Components

As you may already know, Shadow DOM is a part of the Web Components Spec, which offers other neat features, like:

  1. Templates - are used to hold inert markup, which is to be used at a later point in time. By inert, we mean that all the images in the markup are not downloaded, scripts included are not present until the content of the template actually becomes a part of the page.
  2. Decorators - are used to apply the templates based on CSS Selectors and hence can be seen as decorating the existing elements by enhancing their presentation.
  3. HTML Imports - provides us with the capability to reuse other HTML documents in our document without having to explicitly make XHR calls and write event handlers for it.
  4. Custom Elements - allows us to define new HTML element types which can then be used declaratively in the markup. For example, if you want to create your own navigation widget, you define your navigation element, inheriting from HTMLElement and providing certain life-cycle callbacks which implement certain events like construction, change, destruction of the widget and simply use that widget in your markup as <myAwesomeNavigation attr1="value1"..></myAwesomeNavigation>. So custom elements essentially give us a way to bundle all the Shadow DOM magic, hiding the internal details and packages everything together.

I wont babble much about other aspects of the Web Components Spec in this article but it would do us good to remember that together they enable us to create re-usable UI widgets which are portable across browsers in look and feel and fully encapsulated by all the scripts and styles of the consuming page.


Conclusion

The Web Components Spec is a work in progress and the sample code included which works today may not work on a later release. As an example, earlier texts on this subject use the webkitShadowRoot() method which no longer works; Instead use createWebkitShadowRoot() to create a Shadow Root. So if you want to use this to create some cool demos using Shadow DOM, it's always best to refer to the spec for details.

Currently, only Chrome and Opera supports it so I would be wary about including any Shadow DOM on my production instance, but with Google coming out with Polymer which is built on top of Web Components and Polyfills coming out to support Shadow DOM natively, this is surely something that every web developer must get his hands dirty with.

You can also stay updated with the latest happenings on Shadow DOM by following this Google+ Channel. Also checkout the Shadow DOM Visualizer tool, which helps you to visualize how Shadow DOM renders in the browser.

Advertisement