Skip to main content

Real DOM vs Shadow DOM

The Document Object Model (DOM) is the browser's live representation of a webpage. Shadow DOM is a browser feature that attaches an encapsulated DOM subtree to an element, allowing a component to protect its internal markup and styles from accidental outside interference.

Document DOM
└── <user-card> ← Shadow host
├── Light DOM content
└── Shadow root
└── Component internals

One-line idea: The document DOM contains the page, while Shadow DOM creates an encapsulated component subtree inside that same browser DOM.

Core Concepts​

What Is the Document DOM?​

When a browser parses HTML, it creates a tree of objects that JavaScript and browser APIs can access.

<main>
<h1>Hello</h1>
</main>

The resulting structure can be pictured as:

Document
└── main
└── h1
└── "Hello"

Page elements normally belong to this document tree. Styles and selectors can apply broadly unless developers limit their scope.

button {
color: red;
}

This selector affects every matching button in the document's normal tree. In a large application, broad selectors can create naming collisions and unexpected style changes.

What Is Shadow DOM?​

Shadow DOM allows an element to host a separate DOM subtree called a shadow tree. The element that owns it is the shadow host, and the tree begins at a shadow root.

const host = document.querySelector("#card");
const shadowRoot = host.attachShadow({ mode: "open" });

shadowRoot.innerHTML = `
<style>
h2 { color: royalblue; }
</style>
<h2>Account</h2>
`;
Document
└── div#card ← Host
└── #shadow-root ← Shadow root
├── style
└── h2

Shadow DOM is not a replacement for the document DOM. The shadow tree is still real browser DOM and participates in rendering, events and accessibility.

Why Shadow DOM Exists​

Reusable components need reliable boundaries. Without encapsulation, a component may be affected by unrelated selectors or accidentally style other parts of the page.

Shadow DOM mainly provides:

  • DOM encapsulation: Internal nodes are not selected through ordinary document queries.
  • Style scoping: Styles declared inside a shadow tree normally apply only inside it.
  • Component boundaries: Internal implementation can be separated from the public element API.
  • Composition: Slots allow a component to display content supplied by its consumer.

Its primary purpose is maintainability and isolation, not automatic rendering performance.

Light DOM, Shadow Host and Shadow Tree​

Content written as children of a component is commonly called its light DOM.

<user-card>
<span slot="name">Aman</span>
</user-card>

The custom element is the shadow host. Its light DOM child remains in the document tree, while a <slot> inside the shadow tree chooses where that child is displayed.

<user-card> host
├── Light DOM: <span slot="name">Aman</span>
└── Shadow tree: <article><slot name="name"></slot></article>

The browser combines these pieces into the composed tree used for rendering.

Style Encapsulation​

Selectors inside a shadow root do not normally match elements outside that shadow tree. Likewise, ordinary document selectors do not reach internal shadow elements.

/* Inside the shadow root */
h2 {
color: royalblue;
}

This rule targets an internal h2, not every heading on the page.

The boundary is controlled rather than absolute:

  • Inherited CSS properties, such as color, can pass from a host to descendants.
  • CSS custom properties can provide theme values to a component.
  • :host styles the shadow host from inside the shadow tree.
  • ::part() lets a component deliberately expose selected internal parts for styling.
  • ::slotted() targets elements assigned to a slot, with limited selector depth.

Open and Closed Shadow Roots​

The mode option controls whether outside code can retrieve the root through the host's shadowRoot property.

const openRoot = openHost.attachShadow({ mode: "open" });
console.log(openHost.shadowRoot === openRoot); // true

const closedRoot = closedHost.attachShadow({ mode: "closed" });
console.log(closedHost.shadowRoot); // null
Modehost.shadowRootTypical reason
openReturns the shadow rootEasier integration, testing and debugging
closedReturns nullHides the root from that public access path

A closed root is not a security feature. Code that creates it still receives the returned ShadowRoot, and the component runs in the same page environment.

Events Across the Boundary​

Events can cross a shadow boundary depending on their configuration. When an event is observed outside the component, event retargeting can make its target appear to be the shadow host instead of an internal element.

Internal button clicked
↓
Event crosses shadow boundary
↓
Outside listener sees the component host as the target

This protects internal implementation details. When the original path matters, event.composedPath() reports the event's path according to the browser's shadow-DOM rules.

Document DOM vs Shadow DOM​

AreaDocument or Light DOMShadow DOM
RoleHolds the page and component consumersHolds encapsulated component internals
ScopeShared document treeSubtree attached to a host
StylingOrdinary document selectorsStyles are scoped to the shadow tree
QueryingAccessible through document queriesQueried through its ShadowRoot when available
CompositionProvides normal child elementsUses slots to place supplied content
Main useGeneral page structureReusable, isolated component implementation

Practical Shadow DOM Patterns​

Build a Small Web Component​

This custom element renders an encapsulated card and exposes a named slot for its title.

user-card.js
class UserCard extends HTMLElement {
constructor() {
super();

const shadow = this.attachShadow({ mode: "open" });

shadow.innerHTML = `
<style>
:host {
display: block;
}

article {
border: 1px solid #d0d7de;
border-radius: 8px;
padding: 16px;
}

h2 {
color: royalblue;
margin-top: 0;
}
</style>

<article>
<h2><slot name="name">Unknown user</slot></h2>
<slot></slot>
</article>
`;
}
}

customElements.define("user-card", UserCard);

Use it from the document:

index.html
<user-card>
<span slot="name">Aman</span>
<p>Frontend developer</p>
</user-card>

<script type="module" src="./user-card.js"></script>

How it works:

  • <user-card> becomes the shadow host.
  • The component's template and CSS live in its shadow tree.
  • The named slot displays the supplied name.
  • The unnamed slot displays the remaining light DOM content.
  • The internal h2 rule does not style document headings outside the component.

Communicate Through a Public API​

Consumers should not depend on internal shadow elements. Prefer attributes, properties, methods and custom events as the component's public contract.

status-badge.js
class StatusBadge extends HTMLElement {
static observedAttributes = ["status"];

constructor() {
super();
this.attachShadow({ mode: "open" });
}

connectedCallback() {
this.render();
}

attributeChangedCallback() {
this.render();
}

render() {
const status = this.getAttribute("status") ?? "unknown";
this.shadowRoot.textContent = `Status: ${status}`;
}
}

customElements.define("status-badge", StatusBadge);
<status-badge status="online"></status-badge>

The status attribute is a stable public input. The component can later change its internal markup without forcing consumers to update their code.

Shadow DOM vs Virtual DOM​

Shadow DOM and Virtual DOM solve different problems.

AreaShadow DOMVirtual DOM
Main purposeComponent encapsulationCalculating UI updates
Provided byBrowserUI library or framework
Main featuresScoped styles, shadow trees and slotsRendering, comparison and reconciliation
Relationship to Real DOMIs actual browser DOMIs an in-memory UI representation
Common useWeb components and design systemsDeclarative libraries such as React

They can also be used together. A library can render content inside a web component's shadow root.

When to Use Shadow DOM​

Consider Shadow DOM when:

  • distributing reusable web components across different applications
  • building a design system that needs strong style boundaries
  • embedding a widget in pages whose CSS you do not control
  • hiding internal component structure behind a stable public API

Avoid adding it automatically when:

  • ordinary CSS modules or naming conventions already provide enough isolation
  • global styling and simple document queries are important
  • the team is not prepared for shadow-aware testing and debugging
  • the component does not need an encapsulation boundary

Benefits, Trade-Offs and Common Mistakes​

Benefits

  • Reduces accidental CSS collisions.
  • Keeps internal DOM structure separate from consumers.
  • Supports reusable components with slots and public APIs.
  • Makes component boundaries explicit.

Trade-offs

  • Testing tools must enter open shadow roots deliberately.
  • Global CSS utilities do not automatically style internal elements.
  • Theming requires planned APIs such as custom properties or parts.
  • Events and focus behavior require awareness of shadow boundaries.
MistakeBetter approach
Saying Shadow DOM replaces the Real DOMExplain that it creates real DOM subtrees inside the browser
Treating a closed root as secure storageUse it only as an encapsulation signal, never as a security boundary
Assuming no styles cross the boundaryRemember inheritance, custom properties, :host, parts and slots
Reaching into internal nodes from application codeExpose attributes, properties, methods and custom events
Confusing Shadow DOM with Virtual DOMSeparate encapsulation from update calculation
Assuming Shadow DOM improves performance automaticallyUse it for isolation and measure performance independently
Ignoring accessibility inside a componentPreserve semantic HTML, names, focus behavior and keyboard support

Official References​

Summary​

Shadow DOM creates an encapsulated browser DOM subtree that protects component structure and styles while allowing controlled composition through slots and public APIs.

Document creates a component host
↓
Host attaches a shadow root
↓
Internal markup and styles stay scoped
↓
Slots place supplied light DOM content
↓
Attributes, methods and events form the public API