Declarative Shadow DOM
Prerequisites: Shadow DOM and slots
Shadow roots created in JavaScript don't exist until the script runs — so server-rendered pages show nothing, or unstyled content, until then. Declarative shadow DOM writes the shadow root directly in HTML:
<user-badge>
<template shadowrootmode="open">
<style>
:host { display: inline-flex; gap: 0.5rem; align-items: center; }
.avatar { border-radius: 50%; }
</style>
<img class="avatar" src="/img/sam.jpg" alt="" width="32" height="32">
<slot></slot>
</template>
Sam Lee
</user-badge>
<script>
const badge = document.querySelector("user-badge");
console.log("shadow root exists:", badge.shadowRoot !== null);
console.log("template left in DOM?", badge.querySelector("template") !== null);
console.log(badge.shadowRoot.querySelector("img").className);
console.log(JSON.stringify(badge.textContent.trim()));
</script>
shadow root exists: true
template left in DOM? false
avatar
"Sam Lee"
- The parser sees
<template shadowrootmode="open">and attaches its contents as the parent's shadow root. Thetemplateelement itself disappears. - It renders before any JavaScript, and even with JavaScript off.
- Later, a
customElements.defineforuser-badgeupgrades the element and findsthis.shadowRootalready there — check for it instead of callingattachShadowagain. shadowrootdelegatesfocusandshadowrootclonablemirror the JavaScript options.
Supported in Chrome and Edge 111+, Safari 16.4+, and Firefox 123+.
Only for the main parser
For security and compatibility, innerHTML does not process
shadowrootmode — the template stays an ordinary, inert template.
Use setHTMLUnsafe() (or Document.parseHTMLUnsafe()) when you really
need it from script:
<div id="a"></div>
<div id="b"></div>
<script>
const html = '<x-box><template shadowrootmode="open"><b>shadow</b></template></x-box>';
document.getElementById("a").innerHTML = html;
document.getElementById("b").setHTMLUnsafe(html);
console.log("innerHTML:", document.querySelector("#a x-box").shadowRoot);
console.log("setHTMLUnsafe:", document.querySelector("#b x-box").shadowRoot.innerHTML);
</script>
innerHTML: null
setHTMLUnsafe: <b>shadow</b>
As the name warns, setHTMLUnsafe doesn't sanitize — never pass it
untrusted input.
When to use it
- Server-side rendering of web components (Lit, Enhance, and other libraries support it).
- Encapsulated styles for server-rendered widgets with no JavaScript at all.
- The cost: the shadow markup is repeated for every instance in the HTML, so pages get larger. Compression helps a lot, because the repeats compress well.