Slots
This lesson explains Web Components slots and how they let you project light DOM content into a component's internal structure for flexible, composable custom elements.
What Are Slots in Web Components?
Slots are placeholders inside a Web Component where consumers can
insert their own markup. They enable composition: the component
controls layout and behavior, while the parent page supplies
content such as titles, text, icons, and buttons.
Slots are especially useful with Shadow DOM because they let you
project light DOM children into an encapsulated internal template
without breaking encapsulation.
| Concept | Description |
| Slot Element | <slot> marks a projection point |
| Default Slot | Receives content without a slot name |
| Light DOM | Markup supplied by the component consumer |
| Projection | Rendering slotted content inside the component |
| Fallback Content | Default markup shown when nothing is slotted |
| Main Benefit | Flexible and composable custom elements |
Basic Default Slot Example
class InfoBox extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<section class="info-box">
<slot>Default info text</slot>
</section>
`;
}
}
customElements.define("info-box", InfoBox);
<info-box>
This profile was updated 5 minutes ago.
</info-box>
The text inside <info-box> is projected into
the default slot and rendered within the component's internal
structure.
How Slot Projection Works
| Step | What Happens |
| 1. Consumer writes markup | Content is placed inside the custom element tags |
| 2. Component defines slots | <slot> marks where content should appear |
| 3. Browser projects content | Slotted nodes render at the slot location |
| 4. DOM ownership stays the same | Slotted nodes remain in the logical light DOM tree |
| 5. Fallback appears if empty | Default slot content is used when no match exists |
Slots with Shadow DOM
class ProfileCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
.card {
padding: 1rem;
border: 1px solid #dbe5f1;
border-radius: 0.75rem;
}
</style>
<article class="card">
<slot></slot>
</article>
`;
}
}
customElements.define("profile-card", ProfileCard);
<profile-card>
<h2>Alex Chen</h2>
<p>Frontend Developer</p>
</profile-card>
Shadow DOM keeps styles encapsulated, while slots allow external
content to appear in the correct place inside the component.
Fallback Content
class EmptyState extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<section class="empty-state">
<slot>
<p>No items to display yet.</p>
</slot>
</section>
`;
}
}
<!-- Uses fallback content -->
<empty-state></empty-state>
<!-- Replaces fallback content -->
<empty-state>
<p>No search results found.</p>
</empty-state>
Fallback content gives components sensible defaults while still
allowing consumers to override the message when needed.
Projecting Multiple Elements
<content-panel>
<h2>Recent Activity</h2>
<p>You signed in from a new device.</p>
<p>Your password was changed yesterday.</p>
<button type="button">Review Activity</button>
</content-panel>
class ContentPanel extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<section class="panel">
<slot></slot>
</section>
`;
}
}
A single default slot can receive multiple child elements. All
unmatched light DOM children are projected into that slot in order.
Listening for Slot Changes
class DynamicPanel extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<section>
<slot></slot>
</section>
`;
this.querySelector("slot").addEventListener(
"slotchange",
() => this.handleSlotChange()
);
}
handleSlotChange() {
const assigned =
this.querySelector("slot").assignedElements();
this.classList.toggle(
"is-empty",
assigned.length === 0
);
}
}
The slotchange event fires when slotted content
changes. Use it to update layout, empty states, or accessibility
attributes dynamically.
Reading Assigned Slot Content
const slot = this.shadowRoot.querySelector("slot");
const assignedNodes = slot.assignedNodes();
const assignedElements = slot.assignedElements();
assignedElements.forEach((element) => {
console.log(element.tagName, element.textContent);
});
| API | Description |
slot.assignedNodes() | Returns nodes assigned to the slot, including text nodes |
slot.assignedElements() | Returns only element nodes assigned to the slot |
slotchange | Event fired when assigned content changes |
Styling Slotted Content
this.shadowRoot.innerHTML = `
<style>
::slotted(h2) {
margin-top: 0;
font-size: 1.25rem;
}
::slotted(p) {
color: #475569;
}
::slotted(button) {
margin-top: 1rem;
}
</style>
<article class="card">
<slot></slot>
</article>
`;
Use ::slotted() inside Shadow DOM styles to apply
presentation rules to projected content. You cannot style slotted
nodes with descendant selectors such as .card h2
from inside the shadow tree.
Composition with Slots
<app-card>
<h2>Team Updates</h2>
<p>Design review is scheduled for Monday.</p>
<app-button>View Calendar</app-button>
</app-card>
Slots encourage composition over configuration. Instead of passing
many string props, consumers assemble meaningful markup inside the
component.
Slots vs Attributes and Properties
| Approach | Best For | Example |
| Slots | Rich markup and nested content | Card body, dialog content, panel footer |
| Attributes | Simple declarative configuration | variant="primary" |
| Properties | JavaScript objects and complex values | element.items = [...] |
| Events | Output from component to parent | item-selected |
Light DOM Content and Shadow DOM Layout
| Part | Owned By | Rendered Where |
| Slotted content | Consumer / light DOM | Visually inside component via slot |
| Internal wrapper | Component / shadow DOM | Inside shadow root |
| Styles in shadow CSS | Component | Apply through ::slotted() |
| Global page styles | App / document | Can still style slotted light DOM nodes |
When to Use Slots
- Consumers need to pass rich HTML content into a component.
- You are building layout shells such as cards, panels, and dialogs.
- You want composable APIs instead of many string-based props.
- Design system components should accept icons, labels, and actions.
- You need fallback content when consumers provide nothing.
- You want to combine Shadow DOM encapsulation with flexible content.
Common Slot Use Cases
- Card headers, bodies, and footers.
- Modal titles and dialog content areas.
- Alert messages and call-to-action buttons.
- Tabs, accordions, and expandable sections.
- Layout components such as sidebars and page shells.
- Empty states and placeholder regions in reusable widgets.
Slot Best Practices
- Use slots for content projection, not for hidden data storage.
- Provide meaningful fallback content where it helps UX.
- Document which slots your component supports.
- Keep slotted markup semantic for accessibility.
- Use
slotchange when layout depends on assigned content. - Style slotted content with
::slotted() inside Shadow DOM. - Prefer composition for complex UI instead of large attribute APIs.
Common Slot Mistakes
- Assuming slotted nodes move permanently into the shadow tree.
- Trying to query slotted content with normal shadow DOM selectors.
- Using slots for values that should be attributes or properties.
- Forgetting fallback content for empty states.
- Not handling
slotchange in dynamic components. - Over-styling slotted content and breaking consumer flexibility.
- Putting non-visual data nodes inside slotted areas unnecessarily.
Key Takeaways
- Slots let consumers project content into Web Components.
- The default
<slot> receives unmatched child content. - Fallback content provides useful defaults when slots are empty.
slotchange and assignedElements() help with dynamic behavior. - Slots make custom elements composable, flexible, and easier to reuse.
Pro Tip
Design components with slots for the parts that change most often,
such as titles, descriptions, icons, and actions. Keep stable
structure and behavior inside the component itself.