Onto declarative Shadow DOM (DSD): defining a shadow root in HTML so components render correctly on first paint without waiting for JavaScript to attach the tree. That matters for SSR, SEO-visible content, and avoiding a flash of unstyled or empty custom elements.
The problem DSD solves
Imperative shadow attachment runs after the parser meets your custom element. Until the script executes, users may see empty tags or unstyled content. Server-rendered HTML usually has no shadow tree at all. DSD lets the server (or static HTML file) ship the shadow template inline.
Declarative shadow root syntax
<site-alert variant="success">
<template shadowrootmode="open">
<style>
:host { display: block; }
.alert { padding: 1rem; border-radius: 4px; background: #e6f4ea; }
</style>
<div class="alert" role="status">
<slot></slot>
</div>
</template>
Profile saved successfully.
</site-alert>
The <template shadowrootmode="open"> child is not rendered as light DOM. The browser converts it into the element’s shadow root automatically. Content after the template becomes slotted light DOM children.
Pairing with custom element upgrades
Your class should detect an existing shadow root before calling attachShadow:
class SiteAlert extends HTMLElement {
connectedCallback() {
if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `<!-- fallback template -->`;
}
this.shadowRoot.querySelector('.alert')
?.setAttribute('data-variant', this.getAttribute('variant') || 'info');
}
}
customElements.define('site-alert', SiteAlert);
JavaScript adds behaviour (dismiss buttons, auto-hide timers) without replacing markup the server already sent. Progressive enhancement again: content is readable before the module loads.
SSR-friendly patterns
- Put meaningful text in light DOM slots – crawlers and no-JS users see it
- Keep critical styles in the declarative template or use adopted stylesheets in script
- Avoid empty custom elements with all content locked only in JS-generated shadow HTML
- Validate output in browsers without DSD support (legacy WebKit) – polyfills or imperative fallback may be needed
Nested shadow trees
Components can nest: a <site-dialog> inside a <app-shell>, each with its own declarative template. Keep slot names explicit and document composition. Deep nesting increases HTML size – balance SSR clarity against payload.
Build tools and WordPress
Static site generators and some PHP templates can emit DSD markup directly. WordPress blocks and shortcodes might assemble the template string server-side. The HTML side is the contract; how you generate it is up to your stack. Test view-source on production URLs – if the shadow template is missing, SSR is not doing its job.

