Slots in Depth — Complete Guide
In this tutorial, you will learn about Slots in Depth. We cover key concepts, practical examples, and best practices to help you master this topic.
Slots are placeholder elements in Shadow Dom that project host content into specific positions, enabling flexible and composable web component layouts.
What You'll Learn
- How named and default slots work in Shadow DOM
- How slot content is projected (not moved) into the shadow tree
- How to style slotted content with ::slotted pseudo-selector
- How to detect slot changes with slotchange events
- How to handle fallback content in slots
Why It Matters
Slots are what make Web Components composable. Without slots, a card component would need configuration options for every possible layout variation. With slots, users can inject any content into defined positions, making components endlessly flexible.
Real-World Use
- A modal component uses slots for header, body, and footer content
- A page layout component uses slots for sidebar, main, and header areas
- A data table uses a slot for custom cell renderers
flowchart LR A[Shadow Tree] --> B[slot name=header] A --> C[slot default] A --> D[slot name=footer] E[Host Content] --> F[h2 slot=header] E --> G[p (default slot)] E --> H[div slot=footer] F --> B G --> C H --> D C --> I[Rendered in shadow]
Named Slots
Named slots allow host content to target specific positions within the shadow tree.
class PageLayout extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
:host { display: flex; flex-direction: column; min-height: 200px; }
.header { background: #f5f5f5; padding: 1rem; }
.main { flex: 1; padding: 1rem; }
.sidebar { background: #eef; padding: 1rem; width: 250px; }
.footer { background: #333; color: white; padding: 1rem; }
.content-area { display: flex; flex: 1; }
::slotted(h1) { margin: 0; }
::slotted(p) { margin: 0; }
</style>
<div class="header">
<slot name="header">
<h1>Default Header</h1>
</slot>
</div>
<div class="content-area">
<div class="sidebar">
<slot name="sidebar">
<p>Default sidebar content</p>
</slot>
</div>
<div class="main">
<slot></slot>
</div>
</div>
<div class="footer">
<slot name="footer">
<p>Default footer</p>
</slot>
</div>
`;
}
}
customElements.define('page-layout', PageLayout);
<page-layout>
<h1 slot="header">My Page</h1>
<nav slot="sidebar">
<a href="#">Home</a>
<a href="#">About</a>
</nav>
<article>
<h2>Main Content</h2>
<p>This is the primary content area.</p>
</article>
<p slot="footer">Copyright 2026</p>
</page-layout>
Expected output: The page renders with the custom header, sidebar navigation, main content, and footer in their correct positions. If a slot is not filled, the fallback content appears.
Default Slot
The unnamed slot captures all host content that is not assigned to a named slot.
class Card extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
.card { border: 1px solid #ddd; border-radius: 8px; overflow: hidden; }
.card-body { padding: 1rem; }
.empty-state { color: #999; text-align: center; padding: 2rem; }
</style>
<div class="card">
<div class="card-body">
<slot>
<div class="empty-state">
<p>No content provided</p>
</div>
</slot>
</div>
</div>
`;
}
}
customElements.define('my-card', Card);
<my-card>
<h3>Card Title</h3>
<p>This content goes to the default slot.</p>
</my-card>
<my-card></my-card>
<!-- Empty card shows fallback: "No content provided" -->
Expected output: The first card shows the title and paragraph in the card body. The second card shows the fallback content because no children were provided.
Styling Slotted Content
Use ::slotted() to style content that is projected into a slot.
class ListComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
:host { display: block; font-family: sans-serif; }
.list-header {
padding: 8px 12px;
background: #f0f0f0;
font-weight: bold;
border-bottom: 2px solid #ddd;
}
/* Style elements slotted into header */
::slotted([slot="header"]) {
margin: 0;
font-size: 1.1em;
}
/* Style direct children in default slot */
::slotted(*) {
padding: 8px 12px;
border-bottom: 1px solid #eee;
display: block;
}
/* Style specific elements */
::slotted(.important) {
background: #fff3cd;
font-weight: bold;
}
/* Style the first slotted item */
::slotted(:first-child) {
border-top: none;
}
.list-footer {
padding: 8px 12px;
background: #f9f9f9;
font-size: 0.9em;
color: #666;
}
</style>
<div class="list-header">
<slot name="header">
<h2>Default List Title</h2>
</slot>
</div>
<slot></slot>
<div class="list-footer">
<slot name="footer">
<span>End of list</span>
</slot>
</div>
`;
}
}
customElements.define('my-list', ListComponent);
<my-list>
<h2 slot="header">Shopping List</h2>
<span>Milk</span>
<span class="important">Eggs (important)</span>
<span>Bread</span>
<span slot="footer">3 items total</span>
</my-list>
Expected output: The header slot content is styled with no margin and larger font. Each list item has padding and bottom border. The "Eggs" item has the yellow background from the .important selector.
slotchange Event
The slotchange event fires when the content of a slot changes (nodes added or removed).
class ObservableList extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
.count { font-size: 0.9em; color: #666; margin-bottom: 8px; }
.list { border: 1px solid #ddd; border-radius: 4px; }
</style>
<div class="count">
Items: <span id="item-count">0</span>
</div>
<div class="list">
<slot id="default-slot"></slot>
</div>
`;
const slot = shadow.getElementById('default-slot');
slot.addEventListener('slotchange', function(event) {
const assignedNodes = this.assignedNodes();
const count = assignedNodes.filter(n => n.nodeType === Node.ELEMENT_NODE).length;
console.log('Slot content changed!');
console.log('Assigned nodes:', assignedNodes.length);
console.log('Element count:', count);
shadow.getElementById('item-count').textContent = count;
});
}
}
customElements.define('observable-list', ObservableList);
// Usage
const list = document.querySelector('observable-list');
// Initial count is 0
// After adding items:
const item1 = document.createElement('span');
item1.textContent = 'Item 1';
list.appendChild(item1);
// slotchange fires: count becomes 1
const item2 = document.createElement('span');
item2.textContent = 'Item 2';
list.appendChild(item2);
// slotchange fires: count becomes 2
Expected output: When items are added to the component, the slotchange event fires and updates the displayed count. The console logs each change.
Fallback Content
Fallback content inside a slot element appears only when no content is assigned to that slot.
class Accordion extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
details { border: 1px solid #ddd; border-radius: 4px; }
summary { padding: 12px; cursor: pointer; background: #f9f9f9; }
.content { padding: 12px; }
.fallback-note { color: #999; font-style: italic; }
</style>
<details>
<summary>
<slot name="title">
<span class="fallback-note">No title provided</span>
</slot>
</summary>
<div class="content">
<slot name="content">
<p class="fallback-note">No content provided</p>
</slot>
<slot name="actions">
<button disabled>No actions available</button>
</slot>
</div>
</details>
`;
}
}
customElements.define('my-accordion', Accordion);
<!-- With all slots filled -->
<my-accordion>
<span slot="title">Section 1</span>
<p slot="content">This is the main content of section 1.</p>
<button slot="actions">Learn More</button>
</my-accordion>
<!-- With only title -->
<my-accordion>
<span slot="title">Section 2</span>
</my-accordion>
Expected output: The first accordion shows the custom title, content, and action button. The second accordion shows the custom title but falls back to "No content provided" for the content slot and a disabled button for the actions slot.
Common Mistakes
- Confusing slot projection with DOM movement — Slotted content is projected (reflected) into the shadow tree but remains in the light DOM.
document.querySelector('my-card').childrenstill shows the slotted content. - Using ::slotted with complex selectors —
::slottedonly works with simple selectors (element, class, attribute).::slotted(.foo .bar)does not work because slotted content itself is not in the shadow tree. - Forgetting that slots are not required — A shadow tree without slots does not display host content. The host children remain invisible. Always include slots if you want to render host content.
- Assuming slotchange fires on initial assignment — slotchange does not fire during initial render. It only fires when the assigned nodes change after the initial assignment.
- Using slot names with spaces — Slot names with spaces or special characters may not work. Use kebab-case or camelCase slot names.
Practice Questions
- What is the difference between a named slot and the default slot? A named slot targets specific content via the slot attribute. The default (unnamed) slot receives all content not assigned to a named slot.
- Does slotted content physically move into the shadow tree? No. Slotted content remains in the light DOM (as children of the host element). The browser projects (reflects) it into the shadow tree for rendering.
- What does
::slotted(div)style? It styles any<div>element that is projected into a slot. It does not style descendants of the slotted div. - Challenge: Create a tab component using slots. The component has a slot for tab labels and a slot for tab panels. Clicking a tab label shows the corresponding panel. Use slotchange to handle dynamically added tabs. The component should have zero JS inside the shadow root except for the tab switching logic.
FAQ
Mini Project
Create a dashboard widget system using slots. Build a DashboardWidget custom element with slots for: header (title and controls), body (main content), and footer (status and actions). Create three different dashboard widgets (weather, news, stock ticker) each using the same component but with different slotted content. Implement a grid layout that arranges widgets responsively.
What's Next
Continue with Lesson 29: DOM Security to learn about XSS prevention, sanitization, and secure DOM manipulation practices.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro