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.