Shadow DOM
This lesson explains Shadow DOM and how it provides encapsulation for Web Components by separating internal markup and styles from the rest of the page while still supporting slots and custom events.
What Is Shadow DOM?
Shadow DOM is a browser feature that lets a custom element attach
a hidden, encapsulated DOM tree. Internal markup and CSS stay
scoped to the component, which helps prevent style conflicts and
keeps implementation details private.
Shadow DOM is one of the core Web Components standards alongside
Custom Elements and HTML Templates. It is widely used in design
systems, component libraries, and reusable widgets.
| Concept | Description |
| Shadow DOM | Encapsulated DOM subtree attached to a host element |
| Shadow Root | The root node of the shadow tree |
| Host Element | The custom element that owns the shadow tree |
| Create API | element.attachShadow({ mode }) |
| Main Benefit | DOM and CSS encapsulation |
| Works With | Slots, templates, and custom events |
Basic Shadow DOM Example
class AlertBanner extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
.alert {
padding: 1rem;
border-radius: 0.5rem;
background: #dbeafe;
color: #1e3a8a;
}
</style>
<div class="alert" role="alert">
<slot></slot>
</div>
`;
}
}
customElements.define("alert-banner", AlertBanner);
<alert-banner>
Your profile was saved successfully.
</alert-banner>
The alert structure and styles live inside Shadow DOM, while the
message content comes from the light DOM through the default slot.
How Shadow DOM Works
| Layer | Description |
| Light DOM | Markup written by the page or component consumer |
| Host Element | The custom element tag in the document |
| Shadow Root | Entry point to the encapsulated internal tree |
| Shadow Tree | Private internal elements and styles |
| Slots | Bridges that project light DOM into shadow markup |
Encapsulation Benefits
<style>
.alert {
background: red;
color: white;
}
</style>
<alert-banner>
This keeps its own styles inside Shadow DOM.
</alert-banner>
Global page CSS does not automatically restyle internal Shadow
DOM nodes. That makes components safer to reuse across apps with
very different stylesheets.
- Internal class names do not collide with page CSS.
- Component styles do not leak out and restyle the page.
- Private DOM structure is hidden from normal document queries.
- Teams can refactor internals without breaking outside selectors.
Open vs Closed Shadow DOM
| Mode | Access | When to Use |
open | element.shadowRoot is available | Most reusable components and design systems |
closed | Shadow root hidden from outside JavaScript | Strong encapsulation for third-party widgets |
this.attachShadow({ mode: "open" });
// this.shadowRoot is available
this.attachShadow({ mode: "closed" });
// this.shadowRoot is null from outside
Shadow DOM with Slots
class AppCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
.card {
border: 1px solid #dbe5f1;
border-radius: 0.75rem;
padding: 1rem;
}
</style>
<article class="card">
<header><slot name="title"></slot></header>
<div><slot></slot></div>
<footer><slot name="footer"></slot></footer>
</article>
`;
}
}
<app-card>
<h2 slot="title">Billing</h2>
<p>Your plan renews tomorrow.</p>
<button slot="footer" type="button">Manage Plan</button>
</app-card>
Slots let Shadow DOM stay encapsulated while still accepting
flexible content from the component consumer.
Styling Inside Shadow DOM
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
:host([variant="danger"]) {
--banner-bg: #fee2e2;
}
.banner {
background: var(--banner-bg, #dbeafe);
}
::slotted(h2) {
margin-top: 0;
}
</style>
<div class="banner">
<slot></slot>
</div>
`;
| Selector | Purpose |
:host | Styles the custom element host |
:host(...) | Styles host based on attribute or state |
::slotted() | Styles projected slotted content |
| CSS custom properties | Allows theming from outside the shadow tree |
Events and Shadow DOM Boundaries
this.shadowRoot.querySelector("button")
.addEventListener("click", () => {
this.dispatchEvent(
new CustomEvent("action-click", {
detail: { action: "save" },
bubbles: true,
composed: true
})
);
});
Events that originate inside Shadow DOM do not automatically cross
the encapsulation boundary. Set composed: true when
parent applications need to listen for them.
Shadow DOM vs Light DOM Only
| Feature | Shadow DOM | Light DOM Only |
| Style encapsulation | Strong by default | Depends on naming discipline |
| DOM privacy | Internal tree is hidden | All nodes are visible |
| Global CSS effect | Limited on internal nodes | Can affect all inner markup |
| Flexibility | Great with slots | Simple for basic components |
| Best For | Design systems and reusable widgets | Simple components with minimal styling risk |
Declarative Shadow DOM
<profile-card>
<template shadowrootmode="open">
<style>
:host { display: block; }
</style>
<article><slot></slot></article>
</template>
<p>Alex Chen</p>
</profile-card>
Declarative Shadow DOM allows a shadow tree to exist in HTML
before JavaScript runs. This helps with server-side rendering and
faster first render for encapsulated components.
When to Use Shadow DOM
- You need reusable components with isolated CSS.
- You want to prevent global styles from breaking widget internals.
- You are building a design system for multiple apps.
- You need private internal structure with public slots and APIs.
- You distribute components across teams or frameworks.
- You want to reduce class name and DOM selector collisions.
When Light DOM May Be Enough
- The component is very simple and styling conflicts are unlikely.
- You need maximum styling control from global app CSS.
- You are prototyping quickly and do not need encapsulation yet.
- Server rendering support is limited and declarative shadow is unavailable.
- The component is used only inside one tightly controlled app.
Common Shadow DOM Use Cases
- Buttons, badges, alerts, and cards in component libraries.
- Form controls with internal labels and validation UI.
- Modals, drawers, tabs, and accordions.
- Embeddable widgets on third-party websites.
- Micro frontend UI packages with isolated styling.
- Framework-agnostic design system primitives.
Shadow DOM Best Practices
- Attach the shadow root once in the constructor.
- Prefer open mode unless closed encapsulation is truly needed.
- Use slots for flexible consumer content.
- Expose theming through CSS custom properties and parts.
- Emit composed custom events when parent apps must listen.
- Keep semantic HTML and accessibility inside the shadow tree.
- Document public styling and interaction APIs clearly.
Common Shadow DOM Mistakes
- Assuming Shadow DOM alone makes components accessible.
- Using Shadow DOM for every component even when unnecessary.
- Forgetting
composed: true on important custom events. - Trying to style internal nodes with regular page CSS selectors.
- Calling
attachShadow() more than once on the same host. - Overusing closed shadow roots and blocking testing or theming.
- Not providing slots or styling hooks for customization.
Key Takeaways
- Shadow DOM encapsulates internal markup and CSS inside custom elements.
- Create it with
attachShadow() and choose open or closed mode intentionally. - Slots and custom events let encapsulated components stay flexible.
- Shadow DOM is ideal for reusable widgets and design systems.
- Good Shadow DOM design balances encapsulation with theming and accessibility.
Pro Tip
Use Shadow DOM when style isolation and private internals matter,
but still expose a clean public API through attributes, slots,
events, CSS custom properties, and documented styling hooks.