Declarative Shadow DOM

We’re pleased to announce that support for the declarative shadow DOM API has been added and enabled by default in Safari Technology Preview 162. To recap, shadow DOM is a part of Web Components, a set of specifications that were initially proposed by Google to enable the creation of reusable widgets and components on the web. Since then these specifications have been integrated into the DOM and HTML standards. Shadow DOM, in particular, provides a lightweight encapsulation for DOM trees by allowing a creation of a parallel tree on an element called a “shadow tree” that replaces the rendering of the element without modifying its own DOM tree.

Up until this point, creating a shadow tree on an element required calling attachShadow() on the element in JavaScript. This meant that this feature was not available when JavaScript is disabled such as in email clients, and it required care to hide the content supposed to be in a shadow tree until relevant scripts are loaded to avoid flush of contents. In addition, many modern websites and web-based applications deploy a technique called “server-side rendering” whereby programs running on a web server generate HTML markup with the initial content for web browsers to consume, instead of fetching content over the network once scripts are loaded. This helps reducing page load time and also improves SEO because the page content is readily available for search engine crawlers to consume. Many server-side-rendering technologies try to eliminate the need for JavaScript for the initial rendering to reduce the initial paint latency and progressively enhance the content with interactivity as scripts and related metadata are loaded. This was, unfortunately, not possible when using shadow DOM because of the aforementioned requirement to use attachShadow().

Declarative shadow DOM addresses these use cases by providing a mechanism to include shadow DOM content in HTML. In particular, specifying a shadowrootmode content attribute on a template element tells web browsers that the content inside of this template element should be put into a shadow tree attached to its parent element. For example, in the following example, the template element with shadowrootmode will attach a shadow root on some-component element with a text node containing “hello, world.” as its sole child node.

<some-component>
    <template shadowrootmode="closed">hello, world.</template>
</some-component>

When scripts are loaded and ready to make this content interactive, the shadow root can be accessed via ElementInternals as follows:

customElements.define('some-component', class SomeComponent extends HTMLElement {
    #internals;
    constructor() {
        super();
        this.#internals = this.attachInternals();

        // This will log "hello, world."
        console.log(this.#internals.shadowRoot.textContent.trim());
    }
});

We designed this API with backwards compatibility in mind. For example, calling attachShadow() on an element with a declarative shadow DOM returns the declaratively attached shadow root with all its children removed instead of failing by throwing an exception. It means that adopting declarative shadow DOM is backwards compatible with existing JavaScript which relies on attachShadow() to create shadow roots. Note that none of the JavaScript parser APIs (such as DOMParser and innerHTML) support declarative shadow DOM by default to avoid creating new cross-site scripting vulnerabilities in existing websites that accepts arbitrary template content (since script elements in such content had been previously inert and would not run).

In addition, we’re introducing the ability to clone shadow roots. Until now, ShadowRoot and its descendant nodes could not be cloned by cloneNode() or importNode(). attachShadow() now takes cloneable flag as an option. When this flag is set to true, existing JavaScript API such as cloneNode() and importNode() will clone ShadowRoot when cloning its shadow host. Declarative shadow DOM automatically sets this flag to true so that declarative shadow DOM which appears inside other template elements can be cloned along with its host. In the following example, the outer template element contains an instance of some-component element and its shadow tree content is serialized using declarative shadow DOM. Cloning template1.content with document.importNode(template1.content, true) will clone some-component as well as its (declaratively defined) shadow tree.

<template id="template1">
    <some-component>
        <template shadowrootmode="closed">hello, world.</template>
    </some-component>
</template>

In summary, declarative shadow DOM introduces an exciting new way of defining a shadow tree in HTML, which will be useful for server-side rendering of Web Components as well as in context where JavaScript is disabled such as email clients. This has been a highly requested feature with lots of discussions among browser vendors. We’re happy to report its introduction in Safari Technology Preview 162.