9 min read

From template to shadow DOM, declaratively

HTML templates keep markup asleep until you need it—declarative shadow DOM lets the parser wake it, without a line of JavaScript.

Last updated: August 26, 2026
Shadows of two perforated monstera leaves cast on a plain white wall
Photo by Content Pixie on Unsplash

Declarative shadow DOM is a newer, alternative approach to creating shadow roots. Earlier, attaching shadow roots was possible only through a JavaScript API. The declarative approach moves the attachment into HTML, allowing browsers to parse and render web components immediately. It unblocks native server-side rendering of web components and solves issues like FOUC.

If you're lost in terms like shadow DOM, web components or custom elements, check the previous two posts of this mini-series:

  1. Intro to web components—what are custom elements?
  2. Every DOM element casts a shadow

But before we move to the shiny new declarative shadow DOM, we need to focus on the older API that makes it possible.

HTML templates

Unlike declarative shadow DOM, HTML templates are not a new feature. They are over a decade old by now! They have been available across all major browsers since 2014-2015, when Safari and Edge caught up.

A <template> is a native HTML element that holds a fragment of markup without rendering it. It's real DOM, not just a string. The browser parses the content but treats it as inert, which means:

  • The content is not rendered—nothing inside shows up on the page.
  • Its content is not active—<script> won't run, <video> won't fetch, etc.
  • IDs inside don't clash with the live document and aren't matched by document queries.

“Why do I even need a template that doesn't render?” you may ask. And I answer reusable markup. The API solves the problem of reusable markup that people used to fake with strings and innerHTML. This combo worked, but the markup wasn't parsed until injection, and escaping was easy to get wrong, which made XSS a real risk.

People also faked reusable markup with hidden elements, but those weren't perfect either. Those elements were still live, picked up by queries; their IDs still collided. They could also run scripts accidentally.

The template syntax fixes both: the markup is genuinely parsed and stays completely inert until you explicitly activate it. Below is a step-by-step description of how it works.

HTML
<template id="card-template">
  <article class="card">
    <h2 class="card__title"></h2>
    <p class="card__body"></p>
  </article>
</template>

<div id="list"></div>
JS
const template = document.getElementById('card-template')
const clone = document.importNode(template.content, true)

clone.querySelector('.card__title').textContent = 'Hello'
clone.querySelector('.card__body').textContent = 'Rendered from a template.'

document.getElementById('list').append(clone)
  1. We create a card template using the <template> element.
  2. We import a copy of the template's content into the current document.
  3. We query the elements and fill the clone with text.
  4. We insert the clone into the live DOM, making it visible. The document fragment dissolves on insertion; the browser only renders the nested <article> element.

Two ways to use HTML templates

You can use HTML templates in two ways:

  1. Document fragment
  2. Declarative shadow DOM

The document fragment use case described in the previous section was the only one for a long time. Now, we're moving to the shiny bit and another use case—declarative shadow DOM.

Declarative shadow DOM

Declarative shadow DOM (DSD) is an alternative syntax to the imperative shadow DOM that allows creating private DOM trees. Even though it's a relatively new feature (available in major browsers since 2024), it doesn't introduce wholly new syntax.

It only introduces new attributes. By adding shadowrootmode to the template element, you're changing it from “inert, JS-activated” into “parser-activated shadow root.”

HTML
<!-- Parser attaches the template as a shadow root immediately -->
<my-card>
  <template shadowrootmode="open">
    <style>
      .title {
        color: blue;
      }
    </style>
    <h2 class="title"><slot></slot></h2>
  </template>
  Hello
</my-card>

The HTML parser encountering the attribute bypasses steps 2-4 of the above process. Under the hood, the mechanism works like this:

  1. The parser calls the attachShadow() method on the parent element.
  2. It moves the template's content into that shadow root.
  3. Finally, it removes the template. The <template> element disappears after parsing.

This process simplifies the manual labor of cloning and inserting content in JavaScript. Instead of nodes in the light DOM, you're getting an encapsulated shadow root.

If you would like to learn more about shadow roots and their API, check my prior post: Every DOM element casts a shadow.

Template's attributes for shadow DOM

Besides the shadowrootmode attribute, there are more options to configure the template element for the shadow DOM use case:

HTML
<template
  shadowrootmode="open"
  shadowrootcustomelementregistry
  shadowrootdelegatesfocus
  shadowrootclonable
  shadowrootserializable
  shadowrootslotassignment="named"
  shadowrootreferencetarget="input-id"
></template>
  • shadowrootmode: It's the only required attribute. It marks a template for shadow DOM and sets the mode—open or closed.
  • shadowrootcustomelementregistry: An opt-out marker—you tell the parser not to bind this shadow root to the global registry. It's a tricky one—the presence of this attribute means setting customElementRegistry to null—no registry at all. Imperatively, null means fall back to the global one.
  • shadowrootdelegatesfocus: Verbose but self-explanatory—the attribute controls the delegation of focus to the first focusable element inside the host.
  • shadowrootclonable: This attribute controls the presence of the shadow root in the copied shadow host (for example, with the cloneNode() method).
  • shadowrootserializable: Similarly, this one controls whether the shadow root can be serialized (for example, with the getHTML() method).
  • shadowrootslotassignment: Sets the slotAssignment property of a shadow root, which controls how light DOM children get assigned to <slot> elements. It can take two values:
    • named: This mode automatically matches children to slots.
    • manual: In this mode, you explicitly assign elements in JavaScript.
  • shadowrootreferencetarget: IDs are scoped to their shadow root. Cross-shadow referencing via IDs and ARIA attributes didn't work. Until the introduction of this attribute. A component can now declare which internal element should receive references aimed at the host. The attribute is experimental, so be aware before using it in production code.

From my previous post, you may know the imperative API for shadow roots. These options for the attachShadow() method are almost a 1:1 match for the declarative attributes.

Imperative optionDeclarative attribute
mode: 'open' | 'closed'shadowrootmode="open" | "closed"
customElementRegistry: registryshadowrootcustomelementregistry
delegatesFocus: trueshadowrootdelegatesfocus
clonable: trueshadowrootclonable
serializable: trueshadowrootserializable
slotAssignment: 'named' | 'manual'shadowrootslotassignment="named" | "manual"
referenceTarget: 'input-id'shadowrootreferencetarget="input-id"

DSD and SSR

Declarative shadow DOM fixed a pretty big issue related to web components—server-side rendering. SSR, in a nutshell, means producing a string of HTML. And attachShadow() is a DOM method—a shadow root existed only as a live object. The actual component internals could only be created after a JS script ran on the client.

DSD offers the missing string form. A server can write a template that a parser can transform into a real shadow root. Such a mechanism improves performance and a few things related to SEO.

DSD and SEO

SSR puts the content in the HTML without the need to construct markup with JavaScript. It improves SEO in a few ways:

  • Rendering during streaming. The parser goes through the markup and attaches shadow roots one by one, incrementally. That improves Core Web Vitals like LCP or CLS, used by Google to rank web pages.
  • Crawling reliability. Google can render JS, but it's not 100% reliable. Other search engines, such as Bing, have inconsistent JavaScript rendering. LLM crawlers and other bots may not run JavaScript at all. That's why SSR is recommended for reliable crawling and indexing.

Summary of web components

This post concludes our mini-series covering web components. We started with custom elements that allow creating custom HTML tags, like <my-element>. Then we learned that every custom element (and some built-ins) can host shadow DOM. It's an encapsulation mechanism that hides the internal complexities of a component with private DOM trees. The imperative syntax in JavaScript may be cumbersome, so this post covered an alternative, declarative approach and its benefits.

Declarative shadow DOM neatly builds upon a decade-old API—templates. Besides offering a more convenient syntax, it removes web components' limitations related to SSR. It's a web standard available natively across all major browsers, so it won't become obsolete like many JS frameworks. However, compared to them, DSD doesn't offer the same DX or features like state management and reactivity.

While DX is a matter of convenience, accessibility is not. ARIA references cross the shadow boundary in one direction only. Reference targeting, experimental as it is, forwards a reference aimed at the host to a single element inside—but an element in that shadow root still can't point at a label in the outer tree. Styling has its gap—there is no declarative equivalent of adoptedStyleSheets or CSS modules, so shadow roots can't share a single stylesheet without JavaScript. However, declarative CSS module scripts are on the table. Despite the limitations, the future of web components looks promising.

Even though some parts of the surrounding ecosystem remain imperative, DSD is a good first step into a declarative future. Declarative shadow DOM is a beneficial addition to the web platform, definitely.

If you're eager to learn more about templates and declarative shadow DOM, check the resources below.

Support me

My website is powered by Next.js, and I'm powered by coffee. You can buy me one to keep this carbon-silicon system working. But don't feel obliged to. Thanks!

Buy me a coffee

A newsletter that sparks curiosity💡

Subscribe to my newsletter and get a monthly dose of:

  • Web development and design news, examples, inspiration
  • Science theories and skepticism
  • My favorite resources, ideas, tools, and other interesting links
I am not a Nigerian prince to offer you opportunities. I do not send spam. Unsubscribe anytime.

Stay curious. Read more

Shadow of a tree cast on a green lawn with autumn forest in the background
20 min read

Every DOM element casts a shadow

Shadow DOM gives your components their own isolated world—scoped styles, encapsulated markup, and a boundary that keeps everything in place.

Read post
Periodic table of elements as a jigsaw puzzle
12 min read

Intro to web components—what are custom elements?

The browser lets you define your own HTML tags—learn how lifecycle callbacks, attributes, and properties make them work.

Read post
Next.js logo
7 min read

Next.js overview in 1000 words

Next.js is one of many static site generators. But it has one feature that stands out from the competition. In this brief overview, I'll try to describe it.

Read post