Skip to content

Declarative Shadow DOM

This lesson explains Declarative Shadow DOM with clear examples, use cases, and best practices so you can build reusable Web Components confidently.

What Is Declarative Shadow DOM?

Declarative Shadow DOM allows you to create Shadow DOM directly in HTML using a <template> element with the shadowrootmode attribute.

Normally, Shadow DOM is created with JavaScript using attachShadow(). Declarative Shadow DOM makes it possible to define Shadow DOM markup on the server or in static HTML.

Concept Description
Declarative Shadow DOM Shadow DOM written directly in HTML
shadowrootmode Attribute used on <template> to create Shadow DOM
open Allows JavaScript access through element.shadowRoot
closed Hides direct access to the shadow root from JavaScript
Main Benefit Server-rendered Web Components with encapsulated HTML and CSS
Common Use SSR, static HTML, design systems, cards, buttons, and reusable UI

Basic Declarative Shadow DOM Example

<user-card>
  <template shadowrootmode="open">
    <style>
      .card {
        border: 1px solid #dee2e6;
        border-radius: 1rem;
        padding: 1rem;
        background: #ffffff;
      }

      h2 {
        color: #0d6efd;
        margin-top: 0;
      }
    </style>

    <article class="card">
      <h2>User Profile</h2>
      <p>This content is inside Declarative Shadow DOM.</p>
    </article>
  </template>
</user-card>

The browser converts the <template shadowrootmode="open"> into a real Shadow Root for the <user-card> element.

JavaScript Shadow DOM vs Declarative Shadow DOM

class UserCard extends HTMLElement {
  constructor() {
    super();

    this.attachShadow({ mode: "open" });

    this.shadowRoot.innerHTML = `
      <style>
        .card {
          border: 1px solid #dee2e6;
          padding: 1rem;
        }
      </style>

      <article class="card">
        <h2>User Profile</h2>
      </article>
    `;
  }
}

customElements.define("user-card", UserCard);
<user-card>
  <template shadowrootmode="open">
    <style>
      .card {
        border: 1px solid #dee2e6;
        padding: 1rem;
      }
    </style>

    <article class="card">
      <h2>User Profile</h2>
    </article>
  </template>
</user-card>

Both examples create Shadow DOM. The first uses JavaScript, while the second uses HTML.

How Declarative Shadow DOM Works

Step What Happens
1. Add host element Create an element like <user-card>
2. Add template Place <template shadowrootmode="open"> inside it
3. Browser parses HTML The template becomes the element shadow root
4. Styles are scoped CSS inside the template applies only inside Shadow DOM
5. Component renders The shadow content appears as encapsulated component UI

shadowrootmode open vs closed

Mode Example JavaScript Access
open <template shadowrootmode="open"> element.shadowRoot returns the shadow root
closed <template shadowrootmode="closed"> element.shadowRoot returns null

Use open for most reusable Web Components because it is easier to inspect, test, and debug.

Declarative Shadow DOM With Slots

<profile-card>
  <template shadowrootmode="open">
    <style>
      .card {
        border: 1px solid #dee2e6;
        border-radius: 1rem;
        padding: 1rem;
      }

      .title {
        color: #0d6efd;
      }
    </style>

    <article class="card">
      <h2 class="title">
        <slot name="title">Default Title</slot>
      </h2>

      <div>
        <slot>Default body content</slot>
      </div>
    </article>
  </template>

  <span slot="title">John Doe</span>
  <p>Frontend Developer</p>
</profile-card>

Slots allow light DOM content to appear inside the Declarative Shadow DOM layout.

Using Declarative Shadow DOM With Custom Elements

<app-counter>
  <template shadowrootmode="open">
    <style>
      button {
        background: #0d6efd;
        color: white;
        border: none;
        border-radius: 0.5rem;
        padding: 0.5rem 1rem;
      }
    </style>

    <button type="button" id="countButton">
      Count: 0
    </button>
  </template>
</app-counter>
class AppCounter extends HTMLElement {
  connectedCallback() {
    this.count = 0;

    const button = this.shadowRoot.querySelector("#countButton");

    button.addEventListener("click", () => {
      this.count += 1;
      button.textContent = `Count: ${this.count}`;
    });
  }
}

customElements.define("app-counter", AppCounter);

Declarative Shadow DOM can render the initial HTML first. JavaScript can then hydrate the component and add behavior.

Declarative Shadow DOM and Server Rendering

<product-card>
  <template shadowrootmode="open">
    <style>
      .card {
        border: 1px solid #dee2e6;
        border-radius: 1rem;
        padding: 1rem;
      }

      .price {
        color: #198754;
        font-weight: 700;
      }
    </style>

    <article class="card">
      <h2>Wireless Mouse</h2>
      <p class="price">$29.99</p>
      <button type="button">Add to Cart</button>
    </article>
  </template>
</product-card>

This pattern is useful when a server sends already-rendered component HTML to the browser. The page can display meaningful content before JavaScript finishes loading.

Declarative Shadow DOM vs Imperative Shadow DOM

Feature Declarative Shadow DOM Imperative Shadow DOM
Created With <template shadowrootmode="open"> attachShadow({ mode: "open" })
Language HTML JavaScript
Server Rendering Good support Requires JavaScript after page load
Initial Content Can be delivered in the HTML response Created only after JavaScript runs
Best For SSR, static HTML, faster first render Dynamic components and client-only rendering

Using CSS Custom Properties

<app-badge style="--badge-bg: #198754; --badge-color: white;">
  <template shadowrootmode="open">
    <style>
      .badge {
        background: var(--badge-bg, #e7f1ff);
        color: var(--badge-color, #0d6efd);
        border-radius: 999px;
        padding: 0.25rem 0.5rem;
      }
    </style>

    <span class="badge">
      <slot>Badge</slot>
    </span>
  </template>

  Success
</app-badge>

CSS Custom Properties can still pass into Declarative Shadow DOM, making them useful for theming server-rendered components.

Using CSS Parts

<style>
  app-alert::part(box) {
    border-left: 4px solid #0d6efd;
  }

  app-alert::part(title) {
    color: #0d6efd;
  }
</style>

<app-alert>
  <template shadowrootmode="open">
    <article part="box">
      <h2 part="title">Notice</h2>
      <p>This alert exposes parts for external styling.</p>
    </article>
  </template>
</app-alert>

CSS Parts work with Declarative Shadow DOM when internal elements include the part attribute.

When to Use Declarative Shadow DOM

  • You want to create Shadow DOM directly in HTML.
  • You are using server-side rendering for Web Components.
  • You want meaningful component markup before JavaScript loads.
  • You are building static Web Components with scoped styles.
  • You want better first render for component-based pages.
  • You need reusable components that can hydrate later with JavaScript.

Common Declarative Shadow DOM Use Cases

  • Server-rendered cards, buttons, alerts, and badges.
  • Static design system components with scoped CSS.
  • Product cards and content blocks on ecommerce pages.
  • SEO-friendly component markup sent from the server.
  • Hydratable Web Components that add interactivity after loading.
  • Performance-focused pages that need fast initial rendering.

Declarative Shadow DOM Best Practices

  • Use shadowrootmode="open" for most components.
  • Keep critical styles inside the template for encapsulation.
  • Use slots when consumers need to pass content into the component.
  • Use CSS Custom Properties for theming.
  • Use CSS Parts for controlled external styling.
  • Hydrate only when interactivity is needed.
  • Keep server-rendered markup simple and predictable.

Common Declarative Shadow DOM Mistakes

  • Using <template> without shadowrootmode.
  • Expecting Declarative Shadow DOM to add behavior without JavaScript.
  • Forgetting that interactivity still needs hydration or event listeners.
  • Placing the template outside the host element.
  • Using closed mode when debugging or testing needs access.
  • Not providing fallback content for slots.
  • Mixing server-rendered markup and client rendering inconsistently.

Key Takeaways

  • Declarative Shadow DOM creates Shadow DOM using HTML.
  • Use <template shadowrootmode="open"> inside a host element.
  • It is useful for server rendering and static HTML components.
  • Styles inside Declarative Shadow DOM are scoped to the component.
  • JavaScript can hydrate the component later for interactivity.
  • It works well with slots, CSS Custom Properties, and CSS Parts.

Pro Tip

Use Declarative Shadow DOM when you want Web Components to render from HTML first, then add JavaScript behavior later only when needed.