Shadow DOM Introduction — Complete Guide
In this tutorial, you will learn about Shadow DOM Introduction. We cover key concepts, practical examples, and best practices to help you master this topic.
Shadow DOM provides encapsulated DOM subtrees with style isolation, preventing CSS conflicts and enabling self-contained Web Components that work anywhere.
What You'll Learn
- What Shadow DOM is and what problems it solves
- How to attach a shadow root to an element
- The difference between open and closed shadow modes
- How style Encapsulation works in Shadow DOM
- How slotted content and CSS parts enable customization
Why It Matters
CSS conflicts are one of the biggest pain points in web development. Global stylesheets, third-party widgets, and large teams all cause accidental style overrides. Shadow DOM solves this by creating a style boundary between the component and the page.
Real-World Use
- A chat widget embedded on any site without affecting host styles
- A design system component that keeps its internal styles intact
- A third-party analytics badge that cannot be broken by host CSS
flowchart LR A[Host Element] --> B[attachShadow] B --> C[Shadow Root] C --> D[Shadow Tree] D --> E[Encapsulated Styles] D --> F[Slots for Content] B --> G[mode: open] B --> H[mode: closed] G --> I[Accessible via element.shadowRoot] H --> J[Inaccessible from outside]
Attaching a Shadow Root
A shadow root is attached to a host element using attachShadow.
const hostElement = document.querySelector('#widget-host');
// Attach open shadow DOM
const shadowRoot = hostElement.attachShadow({ mode: 'open' });
console.log('Shadow root:', shadowRoot);
console.log('Mode:', shadowRoot.mode);
// Now you can work with shadowRoot like a document fragment
shadowRoot.innerHTML = `
<style>
.container {
padding: 20px;
background: #f0f0f0;
border-radius: 8px;
font-family: sans-serif;
}
h2 { margin: 0 0 10px; color: #333; }
p { color: #666; }
</style>
<div class="container">
<h2>Shadow DOM Widget</h2>
<p>This content is encapsulated. Host CSS cannot touch it.</p>
</div>
`;
// The element's original content is hidden
// unless you use slots
Expected output: The host element displays the styled widget. The host page's CSS does not affect the widget's styles (no font-family bleed, no color override). The widget's styles do not leak out.
Open vs Closed Mode
The mode determines whether external JavaScript can access the shadow tree.
// Open mode (accessible from outside)
const openHost = document.querySelector('#open-host');
const openShadow = openHost.attachShadow({ mode: 'open' });
openShadow.innerHTML = '<p>Open shadow content</p>';
// External code can access:
console.log('Open shadow:', document.querySelector('#open-host').shadowRoot);
// Closed mode (inaccessible from outside)
const closedHost = document.querySelector('#closed-host');
const closedShadow = closedHost.attachShadow({ mode: 'closed' });
closedShadow.innerHTML = '<p>Closed shadow content</p>';
// External code CANNOT access:
console.log('Closed shadow:', document.querySelector('#closed-host').shadowRoot);
// Returns null!
// However, closed mode is not security — it can be bypassed.
// It is more of a hint that the shadow tree is internal.
// Most components should use 'open' for testing and accessibility.
Expected output: The open shadow root is accessible via element.shadowRoot. The closed shadow root returns null. Both render normally on the page.
Style Encapsulation
Styles defined inside Shadow DOM do not affect the host page, and host page styles do not affect the shadow tree.
<!-- Host page -->
<style>
h2 { color: red !important; }
.container { background: yellow !important; }
p { font-size: 24px !important; }
</style>
<div id="demo-host"></div>
const host = document.querySelector('#demo-host');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
h2 { color: blue; }
.container { background: lightblue; }
p { font-size: 14px; }
</style>
<div class="container">
<h2>Shadow H2</h2>
<p>Paragraph inside shadow.</p>
<slot></slot>
</div>
`;
console.log('Host h2 color stays red (outside element)');
console.log('Shadow h2 remains blue (except for inheritable properties)');
// Inheritable properties (color, font-family) DO cascade into
// the shadow tree if not explicitly overridden
// Most other properties are blocked at the shadow boundary
Expected output: The shadow DOM's h2 is blue and the container is lightblue, despite the host page's !important rules. The host page's h2 (if any) stays red. Inheritable properties like color may bleed in if not overridden.
Slots: Projecting Content
Slots allow the host page to inject content into specific positions inside the shadow tree.
const cardHost = document.querySelector('#card-host');
const cardShadow = cardHost.attachShadow({ mode: 'open' });
cardShadow.innerHTML = `
<style>
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
}
.card-header {
font-size: 1.2em;
font-weight: bold;
margin-bottom: 8px;
border-bottom: 1px solid #eee;
padding-bottom: 8px;
}
.card-footer {
margin-top: 16px;
border-top: 1px solid #eee;
padding-top: 8px;
font-size: 0.9em;
color: #666;
}
</style>
<div class="card">
<div class="card-header">
<slot name="header">Default Header</slot>
</div>
<div class="card-body">
<slot></slot>
</div>
<div class="card-footer">
<slot name="footer">Default Footer</slot>
</div>
</div>
`;
<!-- Host page usage -->
<div id="card-host">
<span slot="header">Custom Card Title</span>
<p>This is the main body content of the card.</p>
<p>More body content here.</p>
<span slot="footer">Last updated: today</span>
</div>
Expected output: The card shows "Custom Card Title" in the header, the body paragraphs in the center, and "Last updated: today" in the footer. Slotted content is rendered inside the shadow tree but styled by the host page (or by CSS parts).
Styling the Host
The :host pseudo-class styles the host element itself from within the shadow tree.
const buttonHost = document.querySelector('#my-button');
const buttonShadow = buttonHost.attachShadow({ mode: 'open' });
buttonShadow.innerHTML = `
<style>
:host {
display: inline-block;
padding: 0;
border: none;
cursor: pointer;
}
:host(:hover) {
opacity: 0.8;
}
:host(.primary) .btn {
background: #3498db;
color: white;
}
:host([disabled]) {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
:host-context(.dark-theme) .btn {
background: #555;
color: #eee;
}
.btn {
padding: 10px 20px;
background: #2ecc71;
color: white;
border-radius: 4px;
font-size: 14px;
}
</style>
<button class="btn"><slot></slot></button>
`;
// Host page usage:
// <my-button class="primary">Save</my-button>
// <my-button disabled>Cannot Click</my-button>
Expected output: The button styles are encapsulated. The :host styles apply to the custom element itself. Adding the primary class to the host element changes the button color. Disabled state is handled via :host([disabled]).
Common Mistakes
- Trying to attach shadow DOM to elements that do not support it — Some elements (img, input, textarea, iframe) cannot have a shadow root. They throw an error.
- Attaching shadow DOM twice — An element can only have one shadow root. Calling attachShadow twice throws an error.
- Expecting closed mode to provide security — Closed mode prevents accidental access but can be bypassed. It is not a security boundary.
- Forgetting that inherited styles cross the shadow boundary — Properties like color, font-family, and line-height inherit into the shadow tree. Explicitly set them if you want full encapsulation.
- Using querySelector on the host to find shadow content — querySelector on the host element does not search inside the shadow tree. Use
element.shadowRoot.querySelector().
Practice Questions
- What is the main problem Shadow DOM solves? Style and DOM encapsulation — preventing CSS conflicts between components and the host page.
- What is the difference between open and closed shadow mode? Open mode allows external access via
element.shadowRoot. Closed mode returns null for this property. - How do you insert host content into a specific position in the shadow tree? Use named slots in the shadow template and
slot="name"attributes on the host content. - Challenge: Create a tooltip component using Shadow DOM. The tooltip trigger is the host element's content. When hovered, show a tooltip positioned above the trigger. The tooltip styles must be fully encapsulated. Use a slot for the trigger content and a div for the tooltip text.
FAQ
Mini Project
Create a styled alert component using Shadow DOM. The component supports type attribute (info, success, warning, error) that controls the icon and color scheme. Use slots for the title and message content. Add a dismiss button that hides the alert. The shadow tree must be completely encapsulated — no host CSS should affect the alert styling. Include an :host style for the type theming.
What's Next
Continue with Lesson 27: Template Element to learn how the HTML template element enables reusable markup fragments.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro