⚡IntermediateHTML APIs and Web Components
Shadow DOM Basics
Encapsulate styles and markup with Shadow DOM to build truly isolated reusable components.
Overview
Shadow DOM provides style and markup encapsulation for custom elements. A shadow root is a hidden subtree attached to a host element CSS inside the shadow root doesn't affect the page, and page CSS doesn't style shadow DOM elements (by default).
This solves the "CSS collision" problem common in large applications where global styles accidentally style component internals (or vice versa).
Key concepts:
- Shadow host: the regular DOM element that attaches the shadow root
- Shadow root: the root of the shadow tree (element.attachShadow({mode: 'open'})
- Shadow tree: the DOM tree inside the shadow root
- Slots: <slot> elements that allow light DOM content to be projected into the shadow tree
Key Points
- •element.attachShadow({mode: "open"}) creates an open shadow root
- •CSS inside shadow DOM doesn't leak out; page CSS doesn't leak in
- •mode: "open" allows JS access via element.shadowRoot; "closed" blocks it
- •:host selector styles the shadow host element from inside the shadow root
- •<slot> elements project light DOM children into designated positions in the shadow tree
- •CSS custom properties (variables) DO cross shadow boundaries use them for theming
Examples
Basic shadow DOM with encapsulated styles
<fancy-card title="Hello World" description="A shadow DOM card"></fancy-card>
<script>
class FancyCard extends HTMLElement {
connectedCallback() {
// Create shadow root
const shadow = this.attachShadow({ mode: 'open' });
// Styles are encapsulated won't affect the page
const style = document.createElement('style');
style.textContent = `
:host {
display: block;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
font-family: sans-serif;
}
h3 { margin: 0 0 8px; color: #1f2937; }
p { margin: 0; color: #6b7280; }
`;
const title = document.createElement('h3');
title.textContent = this.getAttribute('title');
const desc = document.createElement('p');
desc.textContent = this.getAttribute('description');
shadow.append(style, title, desc);
}
}
customElements.define('fancy-card', FancyCard);
</script>Using <slot> to project content
<my-panel>
<span slot="title">Panel Title</span>
<p>This content goes in the default slot.</p>
<button slot="action">Save</button>
</my-panel>
<script>
class MyPanel extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
:host { display: block; padding: 16px; border: 1px solid #ccc; }
header { display: flex; justify-content: space-between; }
</style>
<header>
<slot name="title"></slot>
<slot name="action"></slot>
</header>
<div class="body">
<slot></slot> <!-- default slot -->
</div>
`;
}
}
customElements.define('my-panel', MyPanel);
</script>Named slots (slot="name") target <slot name="name">; default slot catches remaining content.
Common Mistakes
- Trying to style shadow DOM children from outside with CSS it doesn't work (use CSS custom properties instead)
- Using mode: "closed" unnecessarily it provides no real security and makes debugging harder
- Querying shadow DOM children with document.querySelector use element.shadowRoot.querySelector
Note
CSS custom properties (--my-color: blue) cross shadow boundaries use them to create a "theming API" for your components. The host page sets the variable; the shadow DOM reads it.
Exercises
Write HTML to solve each exercise. Use Preview to see it render in a live iframe, then mark as done.
Loading editor…
Completed: 0 / 5 exercises
Knowledge Check
Question 1 of 4