Declarative Shadow DOM

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

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:

<some-element>
<template shadowroot="open">
shadow root content for some-element
</template>
</some-element>

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

To avoid some important security considerations, Declarative Shadow Roots also can’t be created
using fragment parsing APIs like innerHTML or insertAdjacentHTML(). The only way to parse
HTML with Declarative Shadow Roots applied is to pass a new includeShadowRoots option to
DOMParser:

<script>
const html = `
<div>
<template shadowroot="open"></template>
</div>
`
;
const div = document.createElement('div');
div.innerHTML = html; // No shadow root here
const fragment = new DOMParser().parseFromString(html, 'text/html', {
includeShadowRoots: true
}); // Shadow root here
</script>

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


Print Share Comment Cite Upload Translate
APA
Jason Miller | Sciencx (2024-03-29T11:11:16+00:00) » Declarative Shadow DOM. Retrieved from https://www.scien.cx/2020/09/30/declarative-shadow-dom/.
MLA
" » Declarative Shadow DOM." Jason Miller | Sciencx - Wednesday September 30, 2020, https://www.scien.cx/2020/09/30/declarative-shadow-dom/
HARVARD
Jason Miller | Sciencx Wednesday September 30, 2020 » Declarative Shadow DOM., viewed 2024-03-29T11:11:16+00:00,<https://www.scien.cx/2020/09/30/declarative-shadow-dom/>
VANCOUVER
Jason Miller | Sciencx - » Declarative Shadow DOM. [Internet]. [Accessed 2024-03-29T11:11:16+00:00]. Available from: https://www.scien.cx/2020/09/30/declarative-shadow-dom/
CHICAGO
" » Declarative Shadow DOM." Jason Miller | Sciencx - Accessed 2024-03-29T11:11:16+00:00. https://www.scien.cx/2020/09/30/declarative-shadow-dom/
IEEE
" » Declarative Shadow DOM." Jason Miller | Sciencx [Online]. Available: https://www.scien.cx/2020/09/30/declarative-shadow-dom/. [Accessed: 2024-03-29T11:11:16+00:00]
rf:citation
» Declarative Shadow DOM | Jason Miller | Sciencx | https://www.scien.cx/2020/09/30/declarative-shadow-dom/ | 2024-03-29T11:11:16+00:00
https://github.com/addpipe/simple-recorderjs-demo