Skip to content

Declarative Shadow DOM — Complete Guide

DodaTech Updated 2026-06-28 5 min read

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 with shadowrootmode attribute
  • 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

  1. Calling attachShadow() in a component constructor when the declarative parser already created the shadow root, causing a "Already attached" error.
  2. Expecting declarative shadow roots to work without a closing </template> tag.
  3. Using shadowrootmode="closed" and then trying to access element.shadowRoot from JavaScript (which returns null).
  4. Forgetting that declarative Shadow DOM is not supported in older browsers without a Polyfill.
  5. Using <slot> inside declarative shadow without understanding that slot projection still requires the custom element definition.

Practice Questions

  1. What attribute is used on <template> to create a declarative shadow root? shadowrootmode with values "open" or "closed".
  2. Does Declarative Shadow DOM require JavaScript? No. The shadow tree is parsed directly from HTML without JavaScript.
  3. How do you safely handle both declarative and imperative shadow roots? Check this.shadowRoot before calling attachShadow().
  4. 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

What is Declarative Shadow DOM?

Declarative Shadow DOM is a way to define shadow trees directly in HTML using the