Skip to content
AgileLogics
5 min read

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. The template element itself disappears.
  • It renders before any JavaScript, and even with JavaScript off.
  • Later, a customElements.define for user-badge upgrades the element and finds this.shadowRoot already there — check for it instead of calling attachShadow again.
  • shadowrootdelegatesfocus and shadowrootclonable mirror 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.

Related developer tools