Declarative Shadow DOM Explained

Declarative Shadow DOM lets you define Shadow DOM directly in HTML, enabling server-side rendering of Web Components. Here's what it is, why it matters, and how to use it.

U

UIXplor Team

July 10, 2026 · 8 min read

01The Web Components SSR Problem

Traditional Shadow DOM is imperative — you attach a shadow root using JavaScript: `element.attachShadow({ mode: 'open' })`. This works great in client-side rendering but creates a fundamental problem for SSR (Server-Side Rendering):

When the server sends HTML to the browser, there's no JavaScript runtime to call `attachShadow()`. The Shadow DOM is missing until JavaScript loads and executes — causing a flash of unstyled content (FOUC) and breaking above-the-fold rendering performance.

Declarative Shadow DOM solves this by letting you define the shadow root directly in HTML — no JavaScript required.

02The Syntax

html
<my-button>
  <!-- This template becomes the shadow root -->
  <template shadowrootmode="open">
    <style>
      button {
        padding: 8px 20px;
        border-radius: 8px;
        background: #6366f1;
        color: white;
        border: none;
        cursor: pointer;
        font-weight: 600;
      }
      button:hover { background: #4f46e5; }
    </style>
    <!-- slot receives light DOM children -->
    <button><slot></slot></button>
  </template>
  Click me
</my-button>

The `<template shadowrootmode="open">` attribute creates a shadow root declaratively. The browser processes this during HTML parsing — before JavaScript runs.

03shadowrootmode Values

html
<!-- Open: accessible via element.shadowRoot in JS -->
<template shadowrootmode="open">...</template>

<!-- Closed: shadowRoot returns null -->
<template shadowrootmode="closed">...</template>

In practice, use `open` for components you control. Use `closed` only when you intentionally want to prevent external JavaScript from accessing the shadow root.

04Combining with Custom Elements

Declarative Shadow DOM works seamlessly with Custom Elements:

html
<!-- Server renders this HTML -->
<user-avatar name="Sarah" src="/avatar.jpg">
  <template shadowrootmode="open">
    <style>/* component styles */</style>
    <img part="avatar" src="/avatar.jpg" alt="Sarah">
    <span part="name"><slot name="name"></slot></span>
  </template>
  <span slot="name">Sarah</span>
</user-avatar>

<script>
// Client-side enhancement: register the custom element
customElements.define('user-avatar', class extends HTMLElement {
  connectedCallback() {
    // Shadow DOM already exists — don't re-attach!
    const shadow = this.shadowRoot; // already populated from HTML
    // Add event listeners, reactive behavior here
  }
});
</script>

The browser renders the shadow DOM from the HTML immediately. When the JavaScript loads, the custom element class is registered and `connectedCallback` runs — it finds the shadow root already populated.

05Adoptable Stylesheets with DSD

A common concern with Shadow DOM is CSS duplication — each shadow root has its own style tag, so styles repeat in the HTML. The `shadowrootserializable` attribute and Constructable Stylesheets address this:

html
<template shadowrootmode="open" shadowrootserializable>
  <!-- styles here are serialized for streaming -->
  <link rel="stylesheet" href="/component.css">
  <slot></slot>
</template>

For shared styles (design system tokens, typography), use the CSS Layers and the CSS `@layer` rule to define global styles that penetrate shadow roots through the cascade.

06Browser Support

Declarative Shadow DOM (with `shadowrootmode`) reached full cross-browser support in 2024:

• Chrome 90+ ✅ • Firefox 123+ ✅ • Safari 16.4+ ✅ • Edge 90+ ✅

Note: Earlier Chrome supported a `shadowroot` attribute (deprecated). Always use `shadowrootmode`.

07FAQ

Does Declarative Shadow DOM work with React or Next.js? Yes. You can include DSD markup in your JSX or HTML templates. The browser processes it before React hydrates. This is particularly useful for islands architecture frameworks.

How does styling work across the shadow boundary? CSS custom properties (variables) do pierce the shadow boundary. You can use CSS custom properties in the shadow root that are defined in the global stylesheet, enabling design token systems to work across shadow roots.

Can I use `::part()` with DSD? Yes. The `part` attribute exposes named elements through the shadow boundary for external CSS to style:

css
user-avatar::part(avatar) { border-radius: 50%; }