Declarative Shadow DOM for server-rendered web components
Published March 5, 2027
<user-card>
<template shadowrootmode="open">
<style>
:host { display: block; border: 1px solid #ddd; border-radius: 8px; padding: 1rem; }
.name { font-weight: 600; margin: 0; }
</style>
<p class="name"><slot name="name">Anonymous</slot></p>
<p class="bio"><slot name="bio"></slot></p>
</template>
<span slot="name">Ada Lovelace</span>
<span slot="bio">Mathematician and writer.</span>
</user-card>What
A <template shadowrootmode="open"> nested inside a custom element's markup is recognized by the HTML parser itself and attached directly as that element's shadow root — the shadow tree exists the instant the HTML is parsed, with no attachShadow() call and no JavaScript execution required.
Why it matters
Normally, a web component's shadow DOM only exists after its class's constructor/connectedCallback runs attachShadow() and populates it — which means a server-rendered page containing <user-card> renders as an empty, unstyled tag until the component's JS is fetched, parsed, and executed. That gap is the actual reason web components got a reputation for being SSR-hostile compared to frameworks that ship server-rendered HTML directly: the visual result depends on JS having already run, every time, everywhere.
Declarative Shadow DOM removes that dependency for the first render. The server (or a static-site generator) emits the shadow tree's HTML as part of the page, the parser attaches it as a real, functioning shadow root during the initial parse pass, and the component is visually complete and styled before a single script executes. JS is still needed for behavior — a component still needs customElements.define() to run before event listeners attach or its class's methods do anything — but the visual output no longer waits on it.
How it works
The snippet is what a server would emit for a <user-card> component:
- The
<template shadowrootmode="open">is consumed by the parser and becomesuser-card's real shadow root — inspect this in devtools and there's no<template>element left in the tree at all, just a#shadow-rootwith the template's former contents inside it. <style>inside the shadow root is scoped to it automatically, same as with imperative shadow DOM — no build-step CSS scoping needed for this component's styles.- The
<slot>elements project the light-DOM content (<span slot="name">…</span>,<span slot="bio">…</span>) into the shadow tree's layout. This composition works identically whether the shadow root was created declaratively or viaattachShadow(). - A separate, small
customElements.define("user-card", …)(not shown as markup — it's the one piece of actual JS this needs) upgrades the already-rendered element so its class's lifecycle methods and any interactive behavior attach. Until that JS runs, the component looks completely correct; it just isn't interactive yet.
Gotchas
innerHTMLandinsertAdjacentHTML()do not processshadowrootmode— only real HTML parsing (the initial page load, orElement.setHTMLUnsafe()/Document.parseHTMLUnsafe()on the client) creates a shadow root from this syntax. Fetch this markup with JS and drop it in via.innerHTML =and you get a literal, inert<template>sitting in the light DOM, not a shadow root — a common surprise when SSR fragments get injected client-side during a SPA transition instead of being present at initial parse.setHTMLUnsafe()is named "Unsafe" deliberately: it parses likeinnerHTML(no script execution) but performs no additional sanitization. Treat its input exactly as trusted-HTML-only, same trust model asinnerHTMLitself.- Cloning the host node (
cloneNode()) after the parser has already attached its declarative shadow root does not preserve that shadow root by default — an imperatively-created shadow root only survives cloning if it was created withattachShadow({ mode, serializable: true }). - Older Chrome (90–110) used a now-dead
shadowrootattribute (no "mode"). If you find that spelling in older blog posts or code samples, it doesn't work on any current browser —shadowrootmodeis the only spelling that ships today.
Browser support: Chrome/Edge 111+, Firefox 123+, Safari 16.4+. Declarative Shadow DOM has been Baseline "newly available" since February 2024 and is on track to reach "widely available" status in August 2026 — safe to rely on across all major engines by the time this posts.
Related
- Imperative Shadow DOM (
attachShadow()) — the client-only mechanism this supplements; still what you use for any shadow root created after initial parse. - Custom Elements +
connectedCallback— the JS half of the story: what upgrades a declaratively-rendered component into an interactive one.