Skip to content

Declarative Shadow DOM

Declarative Shadow DOM (DSD) lets you attach a shadow root to an element using only HTML, without any JavaScript. This is ideal for server-side rendering (SSR), where the shadow tree can be included in the initial HTML response and the browser attaches it before any JavaScript runs.

Placing a <template shadowrootmode="open"> (or "closed") as a direct child of an element causes the browser to automatically attach a shadow root to the parent and move the template’s content into it.

<my-card>
<template shadowrootmode="open">
<style>
p { color: #0070f3; font-weight: bold; }
</style>
<p>This content lives in the shadow root.</p>
<slot></slot>
</template>
Light DOM content here (goes into the slot)
</my-card>

The <template> tag is consumed by the browser and replaced with an attached shadow root. No JavaScript needed — the element has a shadow root immediately on parse.

When JavaScript does load, customElements.define('my-card', MyCard) upgrades the element. If the class calls attachShadow in its constructor, the browser skips creating a second shadow root (it already exists) and reuses the existing one. This means the server-rendered content is preserved — a critical property for SSR hydration.

class MyCard extends HTMLElement {
connectedCallback() {
// Shadow root already attached declaratively — shadowRoot is available
const shadow = this.shadowRoot; // not null
if (shadow) {
// hydrate: add event listeners, update state, etc.
}
}
}
customElements.define('my-card', MyCard);

Declarative Shadow DOM (the standardized shadowrootmode attribute) is supported in Chrome 111+, Edge 111+, Safari 16.4+, and Firefox 123+ — cross-browser since early 2024, once Firefox shipped it. (An earlier, non-standard shadowroot attribute existed in Chrome 90–110 only; it was removed and replaced by shadowrootmode — don’t confuse the two if you find old tutorials.) In older browsers, the <template shadowrootmode> attribute is unknown and the template remains inert. Include a polyfill for older browsers:

// Polyfill for older browsers
document.querySelectorAll('template[shadowrootmode]').forEach(function(t) {
t.parentElement.attachShadow({ mode: t.getAttribute('shadowrootmode') })
.appendChild(t.content.cloneNode(true));
t.remove();
});

A related pair of methods, shadowRoot.getHTML() and shadowRoot.setHTMLUnsafe(), reached Baseline (cross-browser support) in September 2024. getHTML() serializes a shadow root back to an HTML string — the read-side counterpart to declarative shadow DOM — and setHTMLUnsafe() lets you parse a string containing <template shadowrootmode> markup into a live shadow tree, which innerHTML alone does not do.

The LivePreview below shows a <dsd-card> with a <template shadowrootmode="open"> already in the HTML. The slot receives the light-DOM text. No JavaScript is required for the shadow to attach.

OptionBenefitCost
Declarative Shadow DOM (<template shadowrootmode>)The shadow root exists as soon as HTML is parsed — no waiting for JavaScript to load to render — critical for SSR and first paintThe server has to duplicate the template markup for every instance of the component, growing the HTML payload with the number of components on the page
Regular JavaScript attachShadowSmaller server-side markup, no duplicated templateHas to wait for JavaScript to load and run first — users may see unstyled content (FOUC) or no content at all in the meantime
  • Confusing whether you need to call attachShadow again in connectedCallback — if a shadow root is already attached declaratively, the browser just skips it silently, no error thrown. But if your logic assumes it must always create one itself, you can write initialization in the wrong place and accidentally clobber the existing shadow root.
  • Not checking browser support before deploying — a browser that doesn’t support DSD renders the <template> as a plain visible element, so its inner markup leaks out and shows directly on the page.
  • Not checking this.shadowRoot before hydrating on the client — if client-side code creates a new shadow root over the existing one without checking first, the server-rendered content is lost and gets needlessly re-rendered, throwing away the whole benefit of DSD.

💡 Real-world examples

Lit SSR (@lit-labs/ssr) — renders components as Declarative Shadow DOM on the server and ships the browser complete HTML with the shadow tree included immediately, reducing time-to-first-paint with no re-render needed at hydration. (lit.dev/docs/ssr)

Adobe Spectrum Web Components — is designed to be compatible with SSR and Declarative Shadow DOM so a large design system renders quickly from first paint without waiting for JavaScript to hydrate. (opensource.adobe.com)

Which HTML attribute on a template element triggers Declarative Shadow DOM?
When does the browser attach the shadow root in Declarative Shadow DOM?
What happens to this.shadowRoot in a custom element class when a declarative shadow root already exists?