Declarative Shadow DOM — Complete Guide
In this tutorial, you will learn about Declarative Shadow Dom. We cover key concepts, practical examples, and best practices to help you master this topic.
Declarative Shadow DOM allows defining shadow trees directly in HTML using a template-based syntax, enabling server-side rendering of custom elements with scoped styles.
What You'll Learn
- What Declarative Shadow DOM is and how it differs from the imperative API
- How to use the
<template>element withshadowrootmodeattribute - How Declarative Shadow DOM enables SSR for custom elements
- How to detect and upgrade declarative shadow roots
Why It Matters
Imperative Shadow DOM requires JavaScript to run before content renders. Declarative Shadow DOM lets the server send pre-rendered shadow trees, improving load time and supporting environments where JavaScript is disabled.
flowchart LR
A[HTML sent from server] --> B{Contains declarative shadow?}
B -->|Yes| C[Browser parses shadow tree declaratively]
B -->|No| D[JavaScript needed to create shadow roots]
C --> E[Content visible immediately]
E --> F[JavaScript upgrades later]
D --> G[JavaScript must load first]
G --> H[Content delayed until JS runs]
Basic Declarative Shadow DOM
Use the <template> element with shadowrootmode attribute set to "open" or "closed".
<my-component>
<template shadowrootmode="open">
<style>
p { color: blue; font-weight: bold; }
</style>
<p>This text is inside Shadow DOM</p>
</template>
</my-component>
<!-- The browser parses this directly.
No JavaScript needed for the shadow root to exist.
The paragraph appears blue and bold immediately. -->
How the Browser Parses It
When the HTML parser encounters <template shadowrootmode="open">, it creates a shadow root and consumes the template content as the shadow tree. The template element itself is removed from the DOM.
<!-- HTML source -->
<div id="host">
<template shadowrootmode="open">
<span>Declarative content</span>
</template>
</div>
<!-- After parsing, equivalent JavaScript:
const host = document.getElementById('host');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<span>Declarative content</span>';
-->
Detecting Declarative Shadow Roots
You can check if a shadow root was created declaratively using the shadowRoot property.
<div id="demo">
<template shadowrootmode="open">
<p>Hello from declarative Shadow DOM</p>
</template>
</div>
<script>
const host = document.getElementById('demo');
console.log('Shadow root:', host.shadowRoot);
console.log('Is declarative?', host.shadowRoot.mode === 'open');
// Access elements inside the declarative shadow root
const p = host.shadowRoot.querySelector('p');
console.log('Text:', p.textContent);
// Output: Text: Hello from declarative Shadow DOM
</script>
Server-Side Rendering with Declarative Shadow DOM
The main benefit is SSR. The server returns a fully rendered custom element that works without JavaScript, then JavaScript enhances it.
<!-- Server-rendered HTML -->
<user-card username="jdoe">
<template shadowrootmode="open">
<style>
.card { border: 1px solid #ddd; padding: 16px; border-radius: 8px; }
.name { font-size: 1.2em; font-weight: bold; }
.bio { color: #666; }
</style>
<div class="card">
<div class="name" id="displayName">John Doe</div>
<div class="bio" id="displayBio">Software developer</div>
</div>
</template>
</user-card>
<script>
// Enhancement phase: add interactivity
class UserCard extends HTMLElement {
constructor() {
super();
// Shadow root already exists from declarative parsing
// No need to call attachShadow
}
connectedCallback() {
// The shadow root is already populated
const nameEl = this.shadowRoot.getElementById('displayName');
const bioEl = this.shadowRoot.getElementById('displayBio');
// Add event listeners, dynamic behavior, etc.
this.addEventListener('click', () => {
console.log('Card clicked:', nameEl.textContent);
});
}
}
customElements.define('user-card', UserCard);
</script>
Handling Imperative Upgrades After Declarative Creation
When a custom element class is defined after the declarative shadow root exists, the constructor runs on an element that already has a shadow root. Calling attachShadow again would throw.
class SafeComponent extends HTMLElement {
constructor() {
super();
// Check if shadow root already exists (from declarative HTML)
if (this.shadowRoot) {
// Shadow root was created declaratively
console.log('Using existing declarative shadow root');
} else {
// No declarative shadow root, create one imperatively
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = '<p>Imperative fallback</p>';
}
}
}
customElements.define('safe-component', SafeComponent);
Multiple Declarative Shadow Roots
A page can have many declarative shadow roots. Each is independently parsed and scoped.
<ul>
<li>
<template shadowrootmode="open">
<style>span { color: red; }</style>
<span>Item 1 shadow</span>
</template>
</li>
<li>
<template shadowrootmode="open">
<style>span { color: green; }</style>
<span>Item 2 shadow</span>
</template>
</li>
<li>
<template shadowrootmode="open">
<style>span { color: blue; }</style>
<span>Item 3 shadow</span>
</template>
</li>
</ul>
<!-- Each span has its own scoped color. No style conflicts. -->
Common Mistakes
- Calling
attachShadow()in a component constructor when the declarative parser already created the shadow root, causing a "Already attached" error. - Expecting declarative shadow roots to work without a closing
</template>tag. - Using
shadowrootmode="closed"and then trying to accesselement.shadowRootfrom JavaScript (which returns null). - Forgetting that declarative Shadow DOM is not supported in older browsers without a Polyfill.
- Using
<slot>inside declarative shadow without understanding that slot projection still requires the custom element definition.
Practice Questions
- What attribute is used on
<template>to create a declarative shadow root?shadowrootmodewith values "open" or "closed". - Does Declarative Shadow DOM require JavaScript? No. The shadow tree is parsed directly from HTML without JavaScript.
- How do you safely handle both declarative and imperative shadow roots? Check
this.shadowRootbefore callingattachShadow(). - What is the main use case for Declarative Shadow DOM? Server-side rendering of custom elements with immediate visual rendering.
Challenge
Build a server-rendered product card component using Declarative Shadow DOM. The card should display an image, title, price, and description. Add a JavaScript enhancement that makes the price clickable to toggle currency display.
FAQ
{{< faq "Can Declarative Shadow DOM be used with closed mode?" "Yes. Use shadowrootmode=\"closed\". However, element.shadowRoot will be null and external code cannot access the shadow tree." >}}Mini Project
Build a server-rendered FAQ accordion component. Use Declarative Shadow DOM for each FAQ item so the accordion structure renders immediately. Add JavaScript enhancement to toggle open/close states on click.
What's Next
Lesson 15: Shadow DOM Performance
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro