Web Components allow developers to build reusable UI elements that work across different applications and frameworks. Shadow DOM is an important part of this technology because it encapsulates a component's internal structure and styles, reducing conflicts with the surrounding page.
Traditionally, creating a shadow root requires JavaScript to run in the browser. The browser then attaches the component's internal markup and styles to that root. This works well for client-rendered applications, but it creates a problem for server-rendered pages: the component's internal content may not be available until JavaScript executes.
Declarative Shadow DOM addresses this problem by allowing developers to define a shadow root directly in HTML. The browser can parse the markup and create the shadow tree without requiring JavaScript to attach it.
This approach can improve the initial rendering experience, support server-side rendering, and make Web Components easier to integrate into applications that generate HTML on the server.
Understanding Shadow DOM
The Shadow DOM provides an encapsulated DOM tree for a component. Its internal elements are separate from the ordinary document tree, and its styles are generally scoped to the component.
For example, consider a custom notification component:
<app-notification>
<p>Order successfully placed.</p>
</app-notification>Without a shadow root, the paragraph is ordinary light DOM content. Styles from the surrounding page may affect it, and component styles may affect other elements unless carefully scoped.
A Web Component can attach a shadow root and place its internal markup inside it:
class AppNotification extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
shadow.innerHTML = `
<style>
.notification {
padding: 12px;
border-radius: 6px;
background: #e8f5e9;
color: #1b5e20;
}
</style>
<div class="notification">
<slot></slot>
</div>
`;
}
}
customElements.define("app-notification", AppNotification);The <slot> element allows content supplied between the component's opening and closing tags to appear inside the component's shadow tree.
This is a common way to encapsulate a component's presentation while allowing the page to supply its content.
The Problem With JavaScript-Only Shadow Roots
In the traditional approach, the browser must execute the component's JavaScript before the shadow root and its internal markup exist.
On a server-rendered page, the initial HTML might contain only the custom element and its light DOM children. Until JavaScript attaches the shadow root, the component may not display as intended.
This can affect:
Initial rendering of reusable components.
Server-side rendering workflows.
Pages that need to display useful content before JavaScript runs.
Applications that progressively enhance server-generated HTML.
Declarative Shadow DOM allows the server to include the shadow tree in the HTML response itself.
What Is Declarative Shadow DOM?
Declarative Shadow DOM uses a <template> element with the shadowrootmode attribute.
For example:
<app-notification>
<template shadowrootmode="open">
<style>
.notification {
padding: 12px;
border-radius: 6px;
background: #e8f5e9;
color: #1b5e20;
}
</style>
<div class="notification">
Order successfully placed.
</div>
</template>
</app-notification>When a supporting browser parses this markup, it can create a shadow root for the <app-notification> element and place the template's contents inside it.
The browser does not need JavaScript to attach that declarative shadow root.
The shadowrootmode="open" attribute creates an open shadow root. An open shadow root can be accessed through the host element's shadowRoot property after it has been created.
A closed shadow root can be declared with shadowrootmode="closed". In that case, the root is not exposed through the host's shadowRoot property, although the component's internal DOM still exists.
How It Works
The process is straightforward:
The server generates HTML containing a custom element and a declarative shadow root template.
The browser parses the response.
A supporting browser creates the shadow root while parsing the HTML.
The template's content becomes part of the shadow tree.
The component can display its internal markup and styles before client-side JavaScript runs.
This allows the server to provide a more complete initial HTML representation of a Web Component.
Building a Server-Rendered Web Component
Consider a product card that displays a product name, description, and price.
The server can return the following HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Product Details</title>
</head>
<body>
<product-card>
<template shadowrootmode="open">
<style>
.card {
max-width: 320px;
padding: 16px;
border: 1px solid #ddd;
border-radius: 8px;
font-family: Arial, sans-serif;
}
.price {
font-size: 1.25rem;
font-weight: bold;
color: #176b36;
}
</style>
<article class="card">
<h2>Wireless Keyboard</h2>
<p>A compact keyboard for everyday work.</p>
<p class="price">₹1,499</p>
</article>
</template>
</product-card>
</body>
</html>The browser can render the product card's shadow tree during HTML parsing.
No JavaScript is required to create the shadow root or display the product details.
This example demonstrates the rendering mechanism. In a real application, a server-side template engine would generate the product name, description, price, and other content from application data.
Keep the Server and Client Markup Consistent
A server-rendered component may later be enhanced by JavaScript. For example, the product card might gain an add-to-cart button, stock information, or interactive pricing details.
The client-side component must account for the possibility that the browser has already created its shadow root.
A typical component constructor should check whether a shadow root already exists before attempting to create one.
class ProductCard extends HTMLElement {
constructor() {
super();
if (this.shadowRoot) {
// A declarative open shadow root already exists.
return;
}
const shadow = this.attachShadow({ mode: "open" });
shadow.innerHTML = `
<style>
article {
padding: 16px;
border: 1px solid #ddd;
border-radius: 8px;
}
</style>
<article>
<slot></slot>
</article>
`;
}
}
customElements.define("product-card", ProductCard);This example assumes an open declarative shadow root. The shadowRoot property returns null for a closed root, so this check cannot detect every possible existing shadow root.
For production components, the server-rendering and client-hydration strategy should be designed together. A component should not blindly call attachShadow() when a compatible shadow root already exists.
Declarative Shadow DOM and Server-Side Rendering
Server-side rendering generates HTML before the page reaches the browser. Frameworks and custom server applications use it to deliver content that can be displayed without waiting for the entire client-side application to start.
Declarative Shadow DOM extends this model to Web Components.
Instead of returning an empty custom element and waiting for JavaScript, the server can send the component's internal markup and styles.
This is particularly useful for:
Reusable components shared by multiple applications.
Product cards and other content-heavy UI elements.
Design systems that use custom elements.
Server-rendered pages with limited client-side JavaScript.
Applications that progressively enhance HTML after the initial render.
However, Declarative Shadow DOM does not automatically make a page faster in every situation. The actual benefit depends on the amount of JavaScript avoided, the HTML payload, the browser, and the component's behavior.
It also does not eliminate the need for JavaScript when a component requires interactive functionality.
Declarative Shadow DOM vs. JavaScript-Created Shadow DOM
Both approaches create shadow roots, but they differ in when and how the root is created.
Feature | Declarative Shadow DOM | JavaScript-created Shadow DOM |
|---|---|---|
Root creation | During HTML parsing in supporting browsers | When JavaScript calls |
Server rendering | Can include the shadow tree in the HTML response | Requires JavaScript to construct the tree unless another rendering mechanism is used |
Initial content | Can be available before component JavaScript executes | Usually appears after the component creates its root |
Interactivity | Still requires JavaScript when behavior is interactive | Can be created and enhanced in the same JavaScript workflow |
Progressive enhancement | Supports server-provided component markup | Depends on how the component is initialized |
Browser compatibility | Requires support for Declarative Shadow DOM | Widely supported in modern browsers, subject to the APIs used |
Declarative Shadow DOM is not a replacement for every JavaScript-created shadow root. It is an additional rendering option for components whose initial structure can be generated on the server.
Handling Existing Shadow Roots During Hydration
Hydration is the process of attaching client-side behavior to HTML that has already been rendered.
For Web Components, hydration can involve registering a custom element, initializing event handlers, and connecting the component to application state.
The important difference is that a declarative shadow root may already exist when the component's JavaScript runs.
Avoid Attaching a Second Shadow Root
Calling attachShadow() on an element that already has an incompatible shadow root can throw an error. There are also API-specific behaviors to consider when calling it on a host with an existing declarative shadow root.
A safer design is to make the component's initialization process aware of server-rendered markup.
For an open shadow root, the component can check this.shadowRoot and reuse it when the existing structure is compatible.
class StatusPanel extends HTMLElement {
constructor() {
super();
const shadow = this.shadowRoot ??
this.attachShadow({ mode: "open" });
if (!shadow.querySelector(".status")) {
shadow.innerHTML = `
<div class="status">
<slot></slot>
</div>
`;
}
}
}
customElements.define("status-panel", StatusPanel);This pattern is suitable only when the component's server-rendered markup follows the expected structure. Production code should validate the markup and attach event listeners or other behavior without unnecessarily replacing existing content.
Replacing the entire shadow tree during hydration may discard server-rendered state, event-related setup, or content that the server generated specifically for the current request.
Account for Closed Shadow Roots
A closed shadow root is not available through this.shadowRoot. Therefore, a component cannot use the same check to discover a closed root that already exists.
If server-side rendering and client-side enhancement are required, choose an explicit strategy for open or closed roots and test it against the browsers and component lifecycle being supported.
Do not assume that all declarative shadow roots can be discovered through the same JavaScript property.
Browser Compatibility and Fallbacks
Declarative Shadow DOM support depends on the browser and its version. Applications that target older browsers should test the feature in their actual support matrix.
If a browser does not support Declarative Shadow DOM, the template may remain an ordinary template element instead of automatically becoming a shadow root. In that case, the component needs a fallback if it must display the same encapsulated structure.
A fallback can inspect the template and attach a shadow root manually:
class FallbackPanel extends HTMLElement {
constructor() {
super();
if (this.shadowRoot) {
return;
}
const template = this.querySelector(
":scope > template[shadowrootmode]"
);
if (!template) {
return;
}
const mode = template.getAttribute("shadowrootmode");
if (mode !== "open" && mode !== "closed") {
return;
}
const shadow = this.attachShadow({ mode });
shadow.append(template.content.cloneNode(true));
template.remove();
}
}
customElements.define("fallback-panel", FallbackPanel);This is a basic fallback example. It assumes the browser has not already created a shadow root and that the template is a direct child of the host. A complete implementation should account for the component's rendering contract and avoid duplicating content or initialization.
When possible, use a tested Web Components library or framework integration rather than building a general-purpose hydration mechanism from scratch.
Best Practices
Render Meaningful Content on the Server
Include the information users need to see immediately. Server rendering is most useful when it produces actual component content rather than an empty shell that still depends on JavaScript.
Keep Shadow DOM Markup Predictable
The server and client should agree on the component's internal structure. Predictable markup makes hydration easier and reduces the risk of duplicated elements or unnecessary DOM replacement.
Separate Rendering From Behavior
Use HTML and CSS to provide the initial presentation. Add JavaScript for interactions such as event handling, dynamic updates, and client-side state.
Test Without JavaScript
Disable JavaScript during testing to confirm that the server-rendered content appears correctly in supported browsers. Also test unsupported browsers to verify that the chosen fallback works.
Consider Accessibility
Use semantic HTML elements and accessible labels for controls. Encapsulation does not remove the need for accessible names, keyboard interaction, appropriate focus behavior, or correct document structure.
Measure the Actual Benefit
Evaluate initial render time, JavaScript execution, HTML payload size, and hydration work. Declarative Shadow DOM can improve the initial experience, but its value should be measured within the complete application.
Common Mistakes to Avoid
Mistake | Why it causes problems | Better approach |
|---|---|---|
Assuming Declarative Shadow DOM adds interactivity | HTML rendering does not automatically provide event handlers | Add client-side behavior when required |
Calling | Initialization may fail or conflict with server-rendered content | Design the component to reuse compatible existing roots |
Assuming | Closed roots are not exposed through that property | Choose and test an explicit closed-root strategy |
Ignoring unsupported browsers | The template may not become a shadow root | Test browser support and provide a fallback |
Replacing all server-rendered markup during hydration | Existing content may be discarded unnecessarily | Reuse compatible markup and enhance it |
Assuming server rendering guarantees better performance | Large HTML payloads and hydration work can offset the benefit | Measure the complete rendering workflow |
Advantages and Disadvantages
Advantages
Allows browsers to create shadow roots directly from HTML.
Supports server-rendered Web Components without requiring JavaScript for initial shadow-tree creation.
Can improve the initial display of component content.
Fits applications that use progressive enhancement.
Allows reusable custom elements to participate in server-side rendering.
Disadvantages
Browser support must be considered for applications with older clients.
Interactive behavior still requires JavaScript.
Server rendering and client hydration must follow a consistent component structure.
Fallback logic can introduce complexity if multiple rendering paths are supported.
Declarative Shadow DOM does not guarantee improved performance in every application.
Summary
Declarative Shadow DOM allows Web Components to include their shadow trees directly in server-generated HTML. A browser that supports the feature can create the shadow root while parsing the document, making the component's initial content available without waiting for JavaScript to attach the root.
Use the template element with shadowrootmode="open" or shadowrootmode="closed" to define the desired shadow root. When adding client-side behavior, account for the possibility that the server-rendered root already exists and avoid unnecessarily replacing compatible markup.
For production applications, verify browser support, provide fallbacks where necessary, and test the hydration process carefully. Declarative Shadow DOM is most useful when the initial component structure can be rendered on the server and enhanced in the browser only when additional behavior is needed.


Join the conversation! Your thoughts help the community grow.