Docs
25.2-Shadow-DOM
25.2 Shadow DOM
Overview
Shadow DOM provides encapsulation for the DOM tree and styles. It allows you to attach a hidden DOM tree to an element, keeping your component's internal structure separate from the main document.
Learning Objectives
- •Understand the Shadow DOM concept
- •Create and attach shadow roots
- •Work with open and closed modes
- •Apply scoped styles
- •Use CSS custom properties for theming
- •Understand slot-based composition
What is Shadow DOM?
Shadow DOM creates an isolated DOM subtree:
<my-component>
#shadow-root
<style>...</style>
<div class="internal">
<slot></slot>
</div>
</my-component>
- •Styles are scoped inside the shadow root
- •Internal structure is hidden from document queries
- •CSS and JavaScript from the document don't affect shadow content
Creating Shadow DOM
Attaching a Shadow Root
class MyComponent extends HTMLElement {
constructor() {
super();
// Attach shadow root
const shadow = this.attachShadow({ mode: 'open' });
// Add content
shadow.innerHTML = `
<style>
p { color: blue; }
</style>
<p>This is in the shadow DOM</p>
`;
}
}
Open vs Closed Mode
// Open mode - shadow root accessible via element.shadowRoot
this.attachShadow({ mode: 'open' });
element.shadowRoot; // Returns the shadow root
// Closed mode - shadow root not accessible
const shadow = this.attachShadow({ mode: 'closed' });
element.shadowRoot; // Returns null
// Must store reference internally for closed mode
this._shadow = shadow;
Shadow DOM Styling
Scoped Styles
class StyledComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
/* Only affects this component */
p {
color: blue;
font-size: 16px;
}
.highlight {
background: yellow;
}
</style>
<p class="highlight">Styled text</p>
`;
}
}
:host Selector
// Style the host element itself
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
padding: 16px;
border: 1px solid #ccc;
}
/* Host with specific attribute */
:host([theme="dark"]) {
background: #333;
color: white;
}
/* Host with class */
:host(.active) {
border-color: blue;
}
/* Host in specific context */
:host-context(.sidebar) {
max-width: 200px;
}
</style>
`;
::slotted Selector
// Style slotted content
this.shadowRoot.innerHTML = `
<style>
/* Style direct children in slot */
::slotted(*) {
margin: 8px 0;
}
/* Style specific slotted elements */
::slotted(h2) {
color: navy;
}
::slotted(.highlight) {
background: yellow;
}
</style>
<slot></slot>
`;
CSS Custom Properties (Theming)
// Component with customizable styles
this.shadowRoot.innerHTML = `
<style>
:host {
--primary-color: blue;
--text-color: #333;
--spacing: 16px;
}
.container {
color: var(--text-color);
padding: var(--spacing);
}
button {
background: var(--primary-color);
color: white;
}
</style>
<div class="container">
<slot></slot>
<button>Action</button>
</div>
`;
/* External CSS can override custom properties */
my-component {
--primary-color: red;
--text-color: navy;
}
Slots and Composition
Default Slot
// Component with default slot
this.shadowRoot.innerHTML = `
<div class="card">
<slot>Default content</slot>
</div>
`;
<my-card>
<p>This replaces the default</p>
</my-card>
Named Slots
// Component with named slots
this.shadowRoot.innerHTML = `
<div class="card">
<header>
<slot name="header">Default Header</slot>
</header>
<main>
<slot>Default Content</slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
`;
<my-card>
<h2 slot="header">Card Title</h2>
<p>Main content goes here</p>
<button slot="footer">Action</button>
</my-card>
Working with Slots Programmatically
class SlotHandler extends HTMLElement {
connectedCallback() {
const slot = this.shadowRoot.querySelector('slot');
// Get assigned nodes
const nodes = slot.assignedNodes();
const elements = slot.assignedElements();
// Listen for slot changes
slot.addEventListener('slotchange', (e) => {
console.log('Slot content changed');
const assigned = e.target.assignedElements();
console.log('New elements:', assigned);
});
}
}
Shadow DOM and Events
Event Retargeting
// Events are retargeted at shadow boundary
shadow.innerHTML = `<button>Click me</button>`;
shadow.querySelector('button').addEventListener('click', (e) => {
console.log(e.target); // <button>
});
// Outside, event.target is the host
hostElement.addEventListener('click', (e) => {
console.log(e.target); // <my-component>
console.log(e.composedPath()); // Full path through shadow
});
Composed Events
// Create event that crosses shadow boundary
button.dispatchEvent(
new CustomEvent('my-event', {
bubbles: true,
composed: true, // Crosses shadow boundary
detail: { data: 'value' },
})
);
Delegating Focus
this.attachShadow({
mode: 'open',
delegatesFocus: true, // Focus delegates to first focusable element
});
Best Practices
- •Use open mode unless you need true encapsulation
- •Expose CSS custom properties for theming
- •Use slots for flexible composition
- •Keep shadow DOM shallow for performance
- •Consider accessibility with proper ARIA attributes
Summary
| Feature | Description |
|---|---|
attachShadow() | Create shadow root |
mode: 'open' | Shadow root accessible |
mode: 'closed' | Shadow root hidden |
:host | Style the host element |
::slotted() | Style slotted content |
<slot> | Content projection |
--custom-property | Themeable styles |