Shadow Root
This lesson explains Shadow Root and how it creates an encapsulated DOM tree inside a custom element, separating internal markup and styles from the rest of the page.
What Is a Shadow Root?
A shadow root is the top node of a Shadow DOM tree attached to a
host element. It contains the component's private internal
markup and styles, separate from the page's light DOM.
You create a shadow root with attachShadow(). Once
attached, the component can render encapsulated structure while
still projecting external content through slots.
| Concept | Description |
| Shadow Root | Root node of an encapsulated DOM subtree |
| Host Element | The custom element that owns the shadow root |
| Create API | element.attachShadow({ mode }) |
| Access API | element.shadowRoot for open mode |
| Main Benefit | Style and DOM encapsulation |
| Works With | Slots, templates, and custom events |
Basic Shadow Root Example
class InfoCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
.card {
padding: 1rem;
border: 1px solid #dbe5f1;
border-radius: 0.75rem;
}
</style>
<article class="card">
<slot></slot>
</article>
`;
}
}
customElements.define("info-card", InfoCard);
<info-card>
<h2>Team Updates</h2>
<p>Design review is scheduled for Monday.</p>
</info-card>
The shadow root holds the card structure and styles, while slotted
light DOM content appears inside the component.
How Shadow Root Works
| Part | Description |
| Host | The custom element in the light DOM, such as <info-card> |
| Shadow Root | The encapsulated root attached to the host |
| Shadow Tree | Internal nodes inside the shadow root |
| Light DOM | Markup supplied by the component consumer |
| Slots | Projection points from light DOM into shadow tree |
Creating a Shadow Root with attachShadow()
class AppButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({
mode: "open"
});
shadow.innerHTML = `
<style>
button {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.5rem;
background: #2563eb;
color: white;
}
</style>
<button type="button">
<slot></slot>
</button>
`;
}
}
Call attachShadow() once, usually in the constructor.
After that, use the returned shadow root or
this.shadowRoot to render internal content.
Open vs Closed Shadow Root
| Mode | Access | When to Use |
open | element.shadowRoot is available | Most components, testing, tooling, theming |
closed | Shadow root is hidden from outside code | Strong encapsulation, third-party widgets |
// Open shadow root
const openHost = document.createElement("info-card");
console.log(openHost.shadowRoot); // ShadowRoot
// Closed shadow root
class SecureWidget extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "closed" });
}
}
const closedHost = new SecureWidget();
console.log(closedHost.shadowRoot); // null
Querying Inside a Shadow Root
class SearchField extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<label for="query">Search</label>
<input id="query" type="search" />
`;
}
connectedCallback() {
this.input =
this.shadowRoot.querySelector("#query");
this.input.addEventListener("input", (event) => {
this.dispatchEvent(
new CustomEvent("search-change", {
detail: { value: event.target.value },
bubbles: true,
composed: true
})
);
});
}
}
Query internal elements through this.shadowRoot.
External page CSS and DOM queries do not reach inside the shadow
tree by default.
Shadow Root with Templates
<template id="panel-template">
<style>
.panel { padding: 1rem; background: #f8fafc; }
</style>
<section class="panel">
<slot name="title"></slot>
<slot></slot>
</section>
</template>
class PanelBox extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
const template =
document.getElementById("panel-template");
this.shadowRoot.appendChild(
template.content.cloneNode(true)
);
}
}
Templates are a common way to populate a shadow root with reusable
structure and scoped CSS.
Declarative Shadow Root
<profile-card>
<template shadowrootmode="open">
<style>
:host { display: block; }
</style>
<article><slot></slot></article>
</template>
<p>Alex Chen</p>
</profile-card>
Declarative Shadow DOM lets the browser create a shadow root from
HTML markup. This is useful for server-side rendering and
progressive enhancement before JavaScript runs.
Shadow Root Encapsulation Boundary
| Feature | Inside Shadow Root | Outside Shadow Root |
| CSS selectors | Scoped to shadow tree | Do not pierce by default |
| DOM queries | Use shadowRoot.querySelector() | Cannot see internal nodes directly |
| Slotted content | Projected visually into slots | Still owned by light DOM |
| Custom events | Need composed: true to escape | Heard by host/page listeners when composed |
Styling the Host Element
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
margin-bottom: 1rem;
}
:host([disabled]) {
opacity: 0.6;
pointer-events: none;
}
</style>
<button type="button">
<slot></slot>
</button>
`;
The :host selector styles the custom element itself
from inside the shadow root, which is useful for layout and state
styling based on host attributes.
Light DOM vs Shadow Root
| Feature | Light DOM | Shadow Root |
| Ownership | Consumer / page markup | Component internal markup |
| Styling | Global page CSS applies | Scoped shadow CSS applies |
| Visibility | Visible in page DOM tree | Hidden behind encapsulation boundary |
| Best For | Flexible slotted content | Private structure and styles |
When to Use a Shadow Root
- You need to prevent internal CSS from leaking out or being overridden.
- You want private DOM structure inside a reusable component.
- You are building design system widgets with consistent styling.
- You need slots for flexible content with encapsulated layout.
- You want to avoid ID and class name collisions across apps.
- You are distributing components to multiple teams or frameworks.
Common Shadow Root Use Cases
- Buttons, badges, alerts, and cards in a component library.
- Form controls with internal labels and validation UI.
- Modals, drawers, and popovers with private layout structure.
- Tabs and accordions with encapsulated panel markup.
- Embeddable third-party widgets with strong style isolation.
- Framework-agnostic UI packages shared across products.
Shadow Root Best Practices
- Create the shadow root once in the constructor with
attachShadow(). - Prefer
mode: "open" unless you have a strong reason not to. - Use templates or render methods to keep shadow markup organized.
- Expose theming through CSS custom properties and
::part(). - Use slots for flexible consumer content instead of hardcoding everything.
- Emit composed custom events when parent apps need to listen.
- Document which parts of the component are public vs internal.
Common Shadow Root Mistakes
- Calling
attachShadow() more than once on the same element. - Assuming global CSS will style internal shadow DOM nodes directly.
- Trying to query internal nodes from the page without open shadow access.
- Forgetting
composed: true on events that must leave the shadow tree. - Putting all content in shadow DOM when slots would be more flexible.
- Using closed mode and making testing or theming unnecessarily difficult.
- Treating shadow encapsulation as a substitute for accessibility work.
Key Takeaways
- A shadow root is the encapsulated DOM tree inside a custom element.
- Create it with
attachShadow() and use open or closed mode intentionally. - Internal markup and CSS stay separate from the page's light DOM.
- Slots still allow flexible external content projection.
- Shadow roots are a core building block of production Web Components.
Pro Tip
Think of the shadow root as the component's private implementation
layer. Keep stable public APIs on the host element with attributes,
properties, events, and slots, and put internal structure inside
the shadow root.