Learn Measure Blog Live About
decorative shadow dome

Declarative Shadow DOM

Declarative Shadow DOM

A new way to implement and use Shadow DOM directly in HTML.

Declarative Shadow DOM is a proposed web platform feature that the Chrome team is looking for feedback on. Try it out using the experimental flag or polyfill.

Shadow DOM is one of the three Web Components standards, rounded out by HTML templates and Custom Elements. Shadow DOM provides a way to scope CSS styles to a specific DOM subtree and isolate that subtree from the rest of the document. The <slot> element gives us a way to control where the children of a Custom Element should be inserted within its Shadow Tree. These features combined enable a system for building self-contained, reusable components that integrate seamlessly into existing applications just like a built-in HTML element.

Until now, the only way to use Shadow DOM was to construct a shadow root using JavaScript:

const host = document.getElementById('host');
const shadowRoot = host.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';

An imperative API like this works fine for client-side rendering: the same JavaScript modules that define our Custom Elements also create their Shadow Roots and set their content. However, many web applications need to render content server-side or to static HTML at build time. This can be an important part of delivering a reasonable experience to visitors who may not be capable of running JavaScript.

The justifications for Server-Side Rendering (SSR) vary from project to project. Some websites must provide fully functional server-rendered HTML in order to meet accessibility guidelines, others choose to deliver a baseline no-JavaScript experience as a way to guarantee good performance on slow connections or devices.

Historically, it has been difficult to use Shadow DOM in combination with Server-Side Rendering because there was no built-in way to express Shadow Roots in the server-generated HTML. There are also performance implications when attaching Shadow Roots to DOM elements that have already been rendered without them. This can cause layout shifting after the page has loaded, or temporarily show a flash of unstyled content ("FOUC") while loading the Shadow Root's stylesheets.

Declarative Shadow DOM (DSD) removes this limitation, bringing Shadow DOM to the server.

Building a Declarative Shadow Root

A Declarative Shadow Root is a <template> element with a shadowroot attribute:

<host-element>
<template shadowroot="open">
<slot></slot>
</template>
<h2>Light content</h2>
</host-element>

A template element with the shadowroot attribute is detected by the HTML parser and immediately applied as the shadow root of its parent element. Loading the pure HTML markup from the above sample results in the following DOM tree:

<host-element>
#shadow-root (open)
<slot>

<h2>Light content</h2>
</slot>
</host-element>

This code sample is following the Chrome DevTools Elements panel's conventions for displaying Shadow DOM content. For example, the character represents slotted Light DOM content.

This gives us the benefits of Shadow DOM's encapsulation and slot projection in static HTML. No JavaScript is needed to produce the entire tree, including the Shadow Root.

Serialization

In addition to introducing the new <template> syntax for creating shadow roots and attaching them to elements, Declarative Shadow Dom also includes a new API for getting the HTML contents of an element. The new getInnerHTML() method works like .innerHTML, but provides an option to control whether shadow roots should be included in the returned HTML:

const html = element.getInnerHTML({includeShadowRoots: true});
`<host-element>
<template shadowroot="open"><slot></slot></template>
<h2>Light content</h2>
</host-element>
`
;

Passing the includeShadowRoots:true option serializes the entire subtree of an element, including its shadow roots. The included shadow roots are serialized using the <template shadowroot> syntax.

In order to preserve encapsulation semantics, any closed shadow roots within an element will not be serialized by default. To include closed shadow roots in the serialized HTML, an array of references to those shadow roots can be passed via a new closedRoots option:

const html = element.getInnerHTML({
includeShadowRoots: true,
closedRoots: [shadowRoot1, shadowRoot2, ...]
});

When serializing the HTML within an element, any closed shadow roots that are present in the closedRoots array will be serialized using the same template syntax as open shadow roots:

<host-element>
<template shadowroot="closed">
<slot></slot>
</template>
<h2>Light content</h2>
</host-element>

Serialized closed shadow roots are indicated by a shadowroot attribute with a value of closed.

Component hydration

Declarative Shadow DOM can be used on its own as a way to encapsulate styles or customize child placement, but it's most powerful when used with Custom Elements. Components built using Custom Elements get automatically upgraded from static HTML. With the introduction of Declarative Shadow DOM, it's now possible for a Custom Element to have a shadow root before it gets upgraded.

A Custom Element being upgraded from HTML that includes a Declarative Shadow Root will already have that shadow root attached. This means the element will have a shadowRoot property already available when it is instantiated, without your code explicitly creating one. It's best to check this.shadowRoot for any existing shadow root in your element's constructor. If there is already a value, the HTML for this component included a Declarative Shadow Root. If the value is null, there was no Declarative Shadow Root present in the HTML or the browser doesn't support Declarative Shadow DOM.

<menu-toggle>
<template shadowroot="open">
<button>
<slot></slot>
</button>
</template>
Open Menu
</menu-toggle>

<script>
class MenuToggle extends HTMLElement {
constructor() {
super();

// Detect whether we have SSR content already:
if (this.shadowRoot) {
// A Declarative Shadow Root exists!
// wire up event listeners, references, etc.:
const button = this.shadowRoot.firstElementChild;
button.addEventListener('click', toggle);
} else {
// A Declarative Shadow Root doesn't exist.
// Create a new shadow root and populate it:
const shadow = this.attachShadow({mode: 'open'});
shadow.innerHTML = `<button><slot></slot></button>`;
shadow.firstChild.addEventListener('click', toggle);
}
}
}

customElements.define('menu-toggle', MenuToggle);
</script>

Custom Elements have been around for a while, and until now there was no reason to check for an existing shadow root before creating one using attachShadow(). Declarative Shadow DOM includes a small change that allows existing components to work despite this: calling the attachShadow() method on an element with an existing Declarative Shadow Root will not throw an error. Instead, the Declarative Shadow Root is emptied and returned. This allows older components not built for Declarative Shadow DOM to continue working, since declarative roots are preserved until an imperative replacement is created.

For newly-created Custom Elements, a new ElementInternals.shadowRoot property provides an explicit way to get a reference to an element's existing Declarative Shadow Root, both open and closed. This can be used to check for and use any Declarative Shadow Root, while still falling back toattachShadow() in cases where one was not provided.

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

const internals = this.attachInternals();

// check for a Declarative Shadow Root:
let shadow = internals.shadowRoot;
if (!shadow) {
// there wasn't one. create a new Shadow Root:
shadow = this.attachShadow({mode: 'open'});
shadow.innerHTML = `<button><slot></slot></button>`;
}

// in either case, wire up our event listener:
shadow.firstChild.addEventListener('click', toggle);
}
}
customElements.define('menu-toggle', MenuToggle);

One shadow per root

A Declarative Shadow Root is only associated with its parent element. This means shadow roots are always colocated with their associated element. This design decision ensures shadow roots are streamable like the rest of an HTML document. It's also convenient for authoring and generation, since adding a shadow root to an element does not require maintaining a registry of existing shadow roots.

The tradeoff of associating shadow roots with their parent element is that it is not possible for multiple elements to be initialized from the same Declarative Shadow Root <template>. However, this is unlikely to matter in most cases where Declarative Shadow DOM is used, since the contents of each shadow root are seldom identical. While server-rendered HTML often contains repeated element structures, their content generally differs–slight variations in text, attributes, etc. Because the contents of a serialized Declarative Shadow Root are entirely static, upgrading multiple elements from a single Declarative Shadow Root would only work if the elements happened to be identical. Finally, the impact of repeated similar shadow roots on network transfer size is relatively small due to the effects of compression.

In the future, it might be possible to revisit shared shadow roots. If the DOM gains support for built-in templating, Declarative Shadow Roots could be treated as templates that are instantiated in order to construct the shadow root for a given element. The current Declarative Shadow DOM design allows for this possibility to exist in the future by limiting shadow root association to a single element.

Timing is everything

Associating Declarative Shadow Roots directly with their parent element simplifies the process of upgrading and attaching them to that element. Declarative Shadow Roots are detected during HTML parsing, and attached immediately when their closing </template> tag is encountered.

<div id="el">
<script>
el.shadowRoot; // null
</script>

<template shadowroot="open">
<!-- shadow realm -->
</template>

<script>
el.shadowRoot; // ShadowRoot
</script>
</div>

Prior to being attached, the contents of a <template> element with the shadowroot attribute are an inert Document Fragment and are not accessible via the .content property like a standard template. This security measure prevents JavaScript from being able to obtain a reference to closed shadow roots. As a result, the contents of a Declarative Shadow Root are not rendered until its closing </template> tag is parsed.

<div>
<template id="shadow" shadowroot="open">
shadow realm
<script>
shadow.content; // null
</script>
</template>
</div>

Parser-only

Declarative Shadow DOM is a feature of the HTML parser. This means that a Declarative Shadow Root will only be parsed and attached for <template> tags with a shadowroot attribute that are present during HTML parsing. In other words, Declarative Shadow Roots can be constructed during initial HTML parsing, or when using innerHTML:

<some-element>
<template shadowroot="open">
shadow realm
</template>
</some-element>

<script>
const div = document.createElement('div');
div.innerHTML = `
<some-element>
<template shadowroot="open">
shadow realm
</template>
</some-element>
`
;
</script>

In all other cases, setting the shadowroot attribute of a <template> element does nothing, and the template remains an ordinary template element:

const div = document.createElement('div');
const template = document.createElement('template');
template.setAttribute('shadowroot', 'open'); // this does nothing
div.appendChild(template);
div.shadowRoot; // null

Server-rendering with style

Inline and external stylesheets are fully supported inside Declarative Shadow Roots using the standard <style> and <link> tags:

<nineties-button>
<template shadowroot="open">
<style>
button {
color: seagreen;
}
</style>
<link rel="stylesheet" href="/comicsans.css" />
<button>
<slot></slot>
</button>
</template>
I'm Blue
</nineties-button>

Styles specified this way are also highly optimized: if the same stylesheet is present in multiple Declarative Shadow Roots, it is only loaded and parsed once. The browser uses a single backing CSSStyleSheet that is shared by all of the shadow roots, eliminating duplicate memory overhead.

Constructable Stylesheets are not supported in Declarative Shadow DOM. This is because there is currently no way to serialize constructable stylesheets in HTML, and no way to refer to them when populating adoptedStyleSheets.

Feature detection and browser support

The Chrome team is tentatively looking at un-flagging Declarative Shadow DOM in Chrome 88. In the meantime, it can be enabled using the Experimental Web Platform Features flag in Chrome 85. Navigate to chrome://flags/#enable-experimental-web-platform-features to find that setting.

As a new web platform API, Declarative Shadow DOM does not yet have widespread support across all browsers. Browser support can be detected by checking for the existence of a shadowroot property on the prototype of HTMLTemplateElement:

function supportsDeclarativeShadowDOM() {
return HTMLTemplateElement.prototype.hasOwnProperty('shadowRoot');
}

Polyfill

Building a simplified polyfill for Declarative Shadow DOM is relatively straightforward, since a polyfill doesn't need to perfectly replicate the timing semantics or parser-only characteristics that a browser implementation concerns itself with. To polyfill Declarative Shadow DOM, we can scan the DOM to find all <template shadowroot> elements, then convert them to attached Shadow Roots on their parent element. This process can be done once the document is ready, or triggered by more specific events like Custom Element lifecycles.

document.querySelectorAll('template[shadowroot]').forEach(template => {
const mode = template.getAttribute('shadowroot');
const shadowRoot = template.parentNode.attachShadow({ mode });
shadowRoot.appendChild(template.content);
template.remove();
}

Further Reading

Last updated: Improve article