Declarative Shadow DOM

The components in <template> and <slot> and Custom Elements create their shadow roots in JavaScript, so nothing appears until the script has run, and server-rendered HTML cannot contain them. Declarative shadow DOM (DSD) fixes both: a <template shadowrootmode="open"> as the first child of an element tells the HTML parser to attach a shadow root to that parent and put the template's content inside; the template itself never enters the DOM. DSD reached Baseline in February 2024 and became Widely available in August 2026.

Server-rendered cards with declarative shadow roots, upgraded later by JavaScriptHTMLLive
<style>body { font: 15px system-ui; margin: 16px; display: flex; gap: 12px; }</style>
<user-badge>
  <template shadowrootmode="open">
    <style>
      :host { display: flex; gap: 10px; align-items: center; padding: 8px 14px;
              border: 1px solid #cfd8dc; border-radius: 999px; }
      .dot { width: 34px; height: 34px; border-radius: 50%; background: #1565c0; }
      button { all: unset; cursor: pointer; color: #1565c0; }
    </style>
    <span class="dot"></span> <slot>Anonymous</slot> <button part="follow">Follow</button>
  </template>
  Ada Lovelace
</user-badge>
<user-badge>Grace Hopper</user-badge>
<script>
  customElements.define('user-badge', class extends HTMLElement {
    constructor() {
      super();
      if (!this.shadowRoot) {             // no server-rendered root: build one on the client
        this.attachShadow({ mode: 'open' }).innerHTML = '<slot></slot> <button>Follow</button>';
      }
      this.shadowRoot.querySelector('button').addEventListener('click', (e) => {
        e.target.textContent = 'Following';
      });
    }
  });
  document.querySelector('user-badge').shadowRoot.querySelector('button').click();
</script>
Browser output of Listing 2.40
Browser output of 40

The first badge rendered, with scoped styles and a slotted name, before any script ran; the script then hydrated it by attaching a click handler to the existing button. The second badge had no template, so the constructor built a plain root on the client. Every server-rendered instance needs its own template. Three more boolean attributes mirror attachShadow() options: shadowrootdelegatesfocus (focusing the host focuses the first focusable element inside), shadowrootclonable (cloneNode() copies the root) and shadowrootserializable (getHTML({ serializableShadowRoots: true }) outputs it).

You rarely hand-write DSD; server renderers emit it. Lit 310,034 's `@lit-labs/ssr` (https://github.com/lit/lit/tree/main/packages/labs/ssr 21,842 ) (4.1.0, still a Labs package) renders Lit components in Node (with Eleventy 95,272 , Astro 17,728 , Next.js 10,514 and Nuxt 37,147 integrations), and @lit-labs/ssr-client hydrates them.