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.
:hoststyles 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
| Mode | host.shadowRoot | Typical reason |
|---|---|---|
open | Returns the shadow root | Easier integration, testing and debugging |
closed | Returns null | Hides 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
| Area | Document or Light DOM | Shadow DOM |
|---|---|---|
| Role | Holds the page and component consumers | Holds encapsulated component internals |
| Scope | Shared document tree | Subtree attached to a host |
| Styling | Ordinary document selectors | Styles are scoped to the shadow tree |
| Querying | Accessible through document queries | Queried through its ShadowRoot when available |
| Composition | Provides normal child elements | Uses slots to place supplied content |
| Main use | General page structure | Reusable, 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.
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:
<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
h2rule 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.
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.
| Area | Shadow DOM | Virtual DOM |
|---|---|---|
| Main purpose | Component encapsulation | Calculating UI updates |
| Provided by | Browser | UI library or framework |
| Main features | Scoped styles, shadow trees and slots | Rendering, comparison and reconciliation |
| Relationship to Real DOM | Is actual browser DOM | Is an in-memory UI representation |
| Common use | Web components and design systems | Declarative 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.
| Mistake | Better approach |
|---|---|
| Saying Shadow DOM replaces the Real DOM | Explain that it creates real DOM subtrees inside the browser |
| Treating a closed root as secure storage | Use it only as an encapsulation signal, never as a security boundary |
| Assuming no styles cross the boundary | Remember inheritance, custom properties, :host, parts and slots |
| Reaching into internal nodes from application code | Expose attributes, properties, methods and custom events |
| Confusing Shadow DOM with Virtual DOM | Separate encapsulation from update calculation |
| Assuming Shadow DOM improves performance automatically | Use it for isolation and measure performance independently |
| Ignoring accessibility inside a component | Preserve 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