Skip to content
💻 🧠 Code 1001 > 🧪📓 Uncategorized > Shadow DOM — “DOM inside DOM”

Shadow DOM — “DOM inside DOM”

The DOM is a programming interface (API) for page code that represents the page as a tree structure of objects.

code

But this openness has a downside. When we create a complex, reusable component (for example, a custom video player or calendar widget), its internal structure and styles become vulnerable. CSS styles from the main page can accidentally “leak” into the component and break its appearance. Similarly, the page’s JavaScript code can unintentionally modify the component’s internal elements, disrupting its logic.

To solve this problem, Shadow DOM exists.

In essence, Shadow DOM is a “DOM inside DOM”. It is a hidden tree of elements attached to a regular page element (called the “host”) but isolated from the main DOM. It allows the developer to create a sealed boundary around the internal structure of a component, protecting it from the outside world.

Shadow DOM lets you attach hidden DOM trees to elements in the regular DOM tree. This shadow tree starts with a shadow root, under which any elements can be added, just like in the regular DOM.

There are several key terms related to Shadow DOM that you should know. The Shadow host is the regular DOM node to which a Shadow DOM is attached. The DOM tree inside the Shadow DOM is known as the Shadow tree. The point where the Shadow DOM ends and the regular DOM begins is called the Shadow boundary. Finally, the root node of the shadow tree is the Shadow root.

DOM

You can manipulate nodes inside Shadow DOM just like regular nodes. The difference is that no code inside Shadow DOM can affect anything outside it, ensuring reliable encapsulation.

Before Shadow DOM became available to web developers, browsers already used it to encapsulate the internal structure of standard elements. For example, the <video></video> element with controls. All you see in the DOM is the <video></video> tag, but it contains several buttons and other controls inside its Shadow DOM.

Creating Shadow DOM

Shadow DOM can be created either imperatively using JavaScript or declaratively directly in HTML.

Imperatively with JavaScript

This method is perfect for client-rendered applications. We select the host element and call the attachShadow() method on it.

<!-- HTML markup -->
<div id="host"></div>
<span>I’m not in the Shadow DOM</span>
// Find the host and attach Shadow DOM
const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });

// Create and append elements to the shadow tree
const span = document.createElement("span");
span.textContent = "I am in the Shadow DOM";
shadow.appendChild(span);

The result on the page will look like this:

I am in the Shadow DOM
I’m not in the Shadow DOM

Declaratively with HTML

For applications where server-side rendering matters, you can define Shadow DOM declaratively using a <template> element with the <code>shadowrootmode attribute.

<div id="host">
  <template shadowrootmode="open">
    <p>This paragraph is inside the Shadow DOM.</p>
    <style>
      p { color: red; } /* These styles will be isolated */
    </style>
  </template>
</div>

When the browser processes this code, it automatically creates a shadow root for

Encapsulation: protection from JavaScript and CSS

The main advantage of Shadow DOM is isolation. Let’s see how it works.

JavaScript encapsulation

Add a button that tries to modify all <span></span> elements on the page.

// ... Shadow DOM creation code ...

const upper = document.querySelector("#upper-button");
upper.addEventListener("click", () => {
  // This selector searches the entire document
  const spans = document.querySelectorAll("span");
  for (const span of spans) {
    span.textContent = span.textContent.toUpperCase();
  }
});

When the button is clicked, only the <span></span> in the main document changes. The element inside Shadow DOM remains untouched because document.querySelectorAll() cannot “look” past the shadow boundary.

Accessing Shadow DOM: shadowRoot property and nested trees

When we call host.attachShadow({ mode: "open" }), we create an “open” Shadow DOM. This means we can access its content from the outside via host.shadowRoot.

// Find spans only inside the shadow tree of a specific host
const spansInShadow = host.shadowRoot.querySelectorAll("span");

If mode: "closed" is used, host.shadowRoot returns null, and external access to the shadow tree is blocked. This is not a strict security mechanism, but rather a convention for developers that the component’s internals should not be touched.

Working with nested shadow trees

In complex component architectures, one custom element can contain other custom elements, each with its own Shadow DOM. To access an element in a deeply nested shadow tree, you must traverse each shadowRoot sequentially.

Example structure:

  • Component <nmbrs-form></nmbrs-form> (main form).
  • Inside it is a
    containing <nmbrs-button></nmbrs-button> (custom button).
  • Inside <nmbrs-button></nmbrs-button> is a real HTML

To access this button from the global context, the path is:

// 1. Find the root component in the main document
const formComponent = document.querySelector('nmbrs-form');

// 2. Enter its shadow tree
const shadowRoot1 = formComponent.shadowRoot;

// 3. Find the nested button component
const buttonComponent = shadowRoot1.querySelector('div div.btn-container nmbrs-button');

// 4. Enter this component's shadow tree
const shadowRoot2 = buttonComponent.shadowRoot;

// 5. Finally, find the button element
const finalButton = shadowRoot2.querySelector('button#button');

As a single chain:

const button = document.querySelector('nmbrs-form').shadowRoot
                      .querySelector('div div.btn-container nmbrs-button').shadowRoot
                      .querySelector('button#button');

This long chain demonstrates the power of encapsulation: to reach internal details, you must explicitly pass through each “boundary.” This makes code more predictable and protects components from accidental changes.

CSS encapsulation

Styles defined on the main page do not affect elements inside Shadow DOM.

/* This style applies only to spans in the main document */
span {
  color: blue;
  border: 1px solid black;
}

The <span></span> inside the shadow tree will not get these styles. This solves the problem of accidental style conflicts.

Styling inside Shadow DOM

Styles defined inside the shadow tree, in turn, do not affect the main page. There are two main ways to add them.

1. Constructable Stylesheets

This method allows creating a CSSStyleSheet object in JavaScript and applying it to one or multiple shadow trees. It’s efficient if you have shared styles for many components.

const sheet = new CSSStyleSheet();
sheet.replaceSync("span { color: red; border: 2px dotted black; }");

const shadow = host.attachShadow({ mode: "open" });
// Apply stylesheet to shadow root
shadow.adoptedStyleSheets = [sheet];
2. <style> element

A simple declarative way is to place a <style> tag directly inside the shadow tree (often inside a <template>).

<template id="my-element">
  <style>
    span {
      color: red;
      border: 2px dotted black;
    }
  </style>
  <span>I am in Shadow DOM</span>
</template>

Shadow DOM and Custom Elements: a perfect match

The full power of Shadow DOM is revealed when creating Custom Elements. Without encapsulation, they would be extremely fragile.

A custom element is a class extending HTMLElement. Typically, the element itself acts as the shadow host, and its internal structure is created inside the shadow tree.

Example of a simple <filled-circle> component:

class FilledCircle extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: "open" });

    // Create internal implementation (e.g., SVG circle)
    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
    circle.setAttribute("r", "50");
    circle.setAttribute("cx", "50");
    circle.setAttribute("cy", "50");
    // Color comes from the host's attribute
    circle.setAttribute("fill", this.getAttribute("color"));

    svg.appendChild(circle);
    shadow.appendChild(svg);
  }
}
customElements.define("filled-circle", FilledCircle);```

Now we can use it in HTML like a regular tag without worrying about its internal structure:

html


“`

Each of these components will be fully encapsulated and protected from the external page.

Leave a Reply

Your email address will not be published. Required fields are marked *