Centric connect.engage.succeed

Chasing shadows and planting trees: the Shadow DOM

Geschreven door Martijn Kloosterman - 07 juni 2017

Martijn Kloosterman
The Shadow DOM sounds like a bad guy from a superhero tale; luckily, it’s nothing quite that sinister. Still, it’s there, lurking in the webpage’s source. Probably even this one you’re looking at right now. Don’t panic, it’s benign. The shadow DOM has been around since there were web browsers that put things on your screen that weren’t just letters. You’ve been using it and had no idea you were. Isn’t that sneaky?

Tree in shadow

What’s this shadow DOM thingy, then?

Have you ever wondered why buttons, input boxes and multi-media elements vary in look between browsers and operating systems but consistently share the same default look between pages?

These elements use HTML internally as well, but this HTML is hidden from the casual viewer. A quick scan through the page’s HTML source for the elusive code will not reveal it: you will just see an innocuously empty element. You are seeing the effect of the shadow DOM.

Let’s look at a <video> element. On the screen, it shows some buttons and an area for the video to play. When inspecting a video element, you could expect to see something like this:

Now open your Chrome Development Tools and activate the option called ‘Show user agent shadow DOM’ in the ‘Elements’ section (F12 to show tools then F1 to show settings). Now if you examine the video element again, you’ll see something like this:

Wow! Where did all that extra HTML come from? You won’t see it when you look at the HTML source for the page. It is injected directly into the DOM by the user agent (the browser you are using). By injecting all that HTML the browser makes all video elements behave the same way.

Alright, that’s pretty nifty. But what use is it to me?

The previously hidden elements as shown above are part of the shadow DOM. Their existence is not really of that much practical use to you, but it shows you a concept: you can introduce bits to the DOM that are isolated from the viewer and the rest of the document, but still work as if they were always there to begin with. (Please note: the shadow DOM is not intended as a security or obfuscation feature. It is open to anyone who checks the DOM itself.) There is an imaginary line between visible and invisible DOM content as indicated by ‘#shadow root’, which you can see in the screenshot above. This is called the shadow boundary. The browser is unaware of this division and will cross it blindly many times as it renders the page for you.

There are two terms for what is pretty much the same thing: shadow DOM and shadow tree. The shadow DOM is the part of the overall DOM that lives within the shadow boundary. When you examine the shadow DOM by clicking it open to reveal the elements within, you see the shadow tree. To potentially confuse matters further, the original element hosting the shadow DOM (the video element in the example above) is sometimes referred to as the light DOM and its content as the light tree. Confused yet? Bear with me, because it gets better: since you can nest shadow DOMs, a shadow DOM can be a light DOM to the shadow DOM it is hosting, like this:

Shadow DOM functionality that we can use ourselves has been around in Chrome since version 35, and was labelled at the time as V0. Since then a lot has changed for shadow DOM functionality, and the specifications have been updated to V1 to reflect this.

What originally started as a method for providing some elements with browser-specific styling has changed a lot. You can find an example-strewn comparison between V0 and V1, as written by one of the W3C authors, here(1). The official, really dull description of Shadow DOM can be found at W3C(2).

When gathering information on this subject, I noticed that many of the blogs using the V1 specs were using broken demo code. There have been some changes to the specs before finalization that weren’t picked up in time. For example, you can now have at most one shadow DOM per element. Some elements no longer accept shadow DOMs at all: any element that the browser already injects with a shadow DOM, you cannot. The only elements that accept a shadow DOM are article, aside, blockquote, body, div, footer, h1, h2, h3, h4, h5, h6, header, main, nav, p, section and span. 

You can check the actual support for Shadow DOM V1 across all available browser on the CanIUse (7) site.

Shadow DOM benefits

  • It is an isolated DOM element and cannot be accidentally accessed by using greedy querySelectors.
  • The shadow DOM CSS does not affect the rest of the page. So, you can work with simple selectors without running the risk of style name clashes. This makes it ideal for use in components. The component can still reply on styles passed to it from the host if desired.
  • Events that happen in the shadow DOM are invisible to the host, unless specifically exposed. They then appear to originate from the element containing the shadow tree instead of the actual element inside the shadow tree.

Disadvantages

  • Browser support is not universal. Chrome, Firefox, Opera, Safari and the Android browser support shadow DOM V1 for the most part. IE will never have it natively. Edge will support it once it’s running on the Chromium engine.
  • You can polyfill shadow DOM support, but it comes with a hefty performance hit: it will perform a lot slower than native support. You can find the polyfill script on webcomponentsJS(4) (along with other interesting implementations of shadow DOM).
  • In short, the shadow DOM as it stands now gives you access to isolated styling, and JavaScript access from the hosting page. It’s ideal for building plugins as it prevents them from messing up your page. Wider browser support is coming, but progress is not as fast as I’d like it to be.

How do I actually use this thing?

Using the shadow DOM itself is pretty simple. An example:

<html>
<head>
    <style>
        div {
            border: dashed 1px black;
        }
    </style>
</head>
<body>
    <div>I'm a div</div>
    <button>I'm a button</button>
    <script>
        var shadowdiv = document.querySelector('div').attachShadow({ mode: 'open' });
        shadowdiv.innerHTML = '<span>This is the shadow DOM calling</span>'
        var shadowbutton = document.querySelector('button').attachShadow({ mode: 'open' });
        shadowbutton.innerHTML = '<span>This is the shadow DOM calling</span>'
    </script>
</body>
</html>

This results in a dashed div and a button:

Shadow Dom dashed div and a button

As you can see, the div has its default message replaced by the shadow root (the old content is still there as a fallback option for browsers not supporting the shadow DOM) and the button ignored the attempt to give it a shadow root, even going as far as generating a JavaScript error in the process.

To demonstrate the isolation, we can attempt to querySelect the newly added elements:

Shadow Dome querySelect the newly added elements

Setting attachShadow mode to ‘closed’ makes the shadow root’s implementation internals inaccessible and unchangeable from JavaScript, which is rarely desirable:

Shadow Dome Setting attachShadow mode  to ‘closed’

You mentioned something about templates?

Yes, indeed, I did. You can create a template inside the shadow root and fill it using elements called slots. They can take content from the hosting element and include this. Much like regular templating in master-detail views. An unnamed slot element will be the default that the content slots into. You can have multiple slots, and distinguish between them by providing unique name properties.

An element can include a property ‘slot’ to target it to specific slots in the shadow DOM. Take a look at the following example:

<html>
<head>
    <style>
        div {
            background-color: #CCC;
        }
    </style>
</head>
<body>
    <div>
        A regular div
    </div>
    <div id="target">
        Some default content
        <span slot="slot1">Some content for slot 1</span>
    </div>
    <div>
        Another regular div
    </div>

    <script>
        var shadowdiv = document.querySelector('#target').attachShadow({ mode: 'open' });
        shadowdiv.innerHTML = '<style>div { background-color: #DDD; }</style><div><slot></slot></div><div><slot name="slot1"></slot1></div>';
    </script>
</body>
</html>

Which will look like this in Chrome:

Shadow Dome in Chrome 

There is a span with a slot property ‘slot1’ in the central div. This will end up in the slot with the same name in the shadow tree:

<div><slot name="slot1"></slot1></div>

The default content goes to the unnamed and, thus, default slot:

<div><slot></slot></div>

Another thing to note is that there’s a global style that makes the default background color for all div elements dark grey. There’s a default div style in the shadow root that states that all div elements must have a light grey background. These do not clash because the shadow root styles are scoped to the shadow root only.

This is how the template behaves at the basic level. You can take it much further since you can have shadow roots in shadow roots, slots in slots. The sky’s the limit, as they say. Combine it with existing model-view libraries to speed these up, or write your own implementation. There is nothing stopping you from implementing your own custom elements. You can find more on web components over at html5rock(5).

What’s next for the shadow DOM?

Shadow DOM has the potential to drastically change the landscape in the coming years: a built-in templating engine that outperforms any JavaScript library is, in my opinion, a potential game changer.

I expect that it will be included in the future of React and Angular or derivatives, further boosting these frameworks’ performance. Tentative implementations have already started to surface. Take for example ReactShadow(6).

For such a shadowy subject, the future is positively bright.

References

  1. https://hayato.io/2016/shadowdomv1
  2. https://www.w3.org/TR/shadow-dom/
  3. https://wpdev.uservoice.com/forums/257854-microsoft-edge-developer/suggestions/6263785-shadow-dom-unprefixed
  4. https://github.com/webcomponents/webcomponentsjs
  5. https://www.html5rocks.com/en/tutorials/webcomponents/shadowdom/
  6. https://github.com/Wildhoney/ReactShadow
  7. https://caniuse.com/#feat=shadowdomv1

Other sources

About Martijn

Craft Expert Martijn Kloosterman is part of the .NET team within Craft, the development programme for IT professionals (powered by Centric). If you would like to follow his blog, sign up for the monthly Craft update.

Want to know more about Craft, the development programme for IT professionals? Check out the website.

Tags:.NET

     
Reacties
    Schrijf een reactie
    • Captcha image
    • Verzenden