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.
Join the conversation! Your thoughts help the community grow.