slotchange Event
This lesson explains the slotchange event and how to react when slotted content changes in Web Components, including default slots, named slots, empty states, and dynamic updates.
What Is the slotchange Event?
The slotchange event fires on a
<slot> element when the set of assigned nodes
changes. It lets a Web Component react when consumers add, remove,
or update slotted content in the light DOM.
This event is useful for empty states, layout updates, validation,
accessibility adjustments, and any behavior that depends on what
content was projected into the component.
| Concept | Description |
| Event Name | slotchange |
| Target | The <slot> element |
| When It Fires | Assigned slotted nodes change |
| Related API | assignedElements(), assignedNodes() |
| Common Use | Empty states and dynamic slot-aware UI |
| Works With | Default slots and named slots |
Basic slotchange Example
class ContentPanel extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<section class="panel">
<slot></slot>
</section>
`;
this.slot = this.querySelector("slot");
this.slot.addEventListener("slotchange", () => {
this.updateEmptyState();
});
this.updateEmptyState();
}
updateEmptyState() {
const hasContent =
this.slot.assignedElements().length > 0;
this.classList.toggle("is-empty", !hasContent);
}
}
customElements.define("content-panel", ContentPanel);
<content-panel>
<p>You have 3 unread messages.</p>
</content-panel>
The component listens for slotchange and toggles an
empty-state class based on whether any elements are assigned to
the slot.
How slotchange Works
| Step | What Happens |
| 1. Consumer updates light DOM | Child nodes are added, removed, or reassigned |
| 2. Browser recalculates assignment | Nodes are matched to default or named slots |
| 3. slotchange fires | The affected <slot> dispatches the event |
| 4. Component reads assigned content | Use assignedElements() or assignedNodes() |
| 5. UI updates | Layout, classes, or accessibility attributes change |
Reading Assigned Slot Content
handleSlotChange(event) {
const slot = event.target;
const elements = slot.assignedElements();
const nodes = slot.assignedNodes();
console.log("Elements:", elements.length);
console.log("Nodes:", nodes.length);
elements.forEach((element) => {
console.log(element.tagName, element.textContent.trim());
});
}
| API | Description |
slot.assignedElements() | Returns element nodes assigned to the slot |
slot.assignedNodes() | Returns all assigned nodes, including text nodes |
event.target | The slot that changed |
slotchange with Named Slots
class AppCard extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<article class="card">
<header><slot name="title"></slot></header>
<div><slot></slot></div>
<footer><slot name="footer"></slot></footer>
</article>
`;
this.querySelectorAll("slot").forEach((slot) => {
slot.addEventListener("slotchange", () => {
this.updateSlotState(slot);
});
});
}
updateSlotState(slot) {
const isEmpty =
slot.assignedElements().length === 0;
slot.parentElement.classList.toggle(
"is-empty",
isEmpty
);
}
}
<app-card>
<h2 slot="title">Billing</h2>
<p>Your plan renews tomorrow.</p>
<button slot="footer" type="button">Manage Plan</button>
</app-card>
Components with multiple named slots often attach a
slotchange listener to each slot so header, body, and
footer regions can update independently.
slotchange in Shadow DOM
class EmptyState extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
:host(.is-empty) .placeholder {
display: block;
}
.placeholder {
display: none;
color: #64748b;
}
</style>
<section>
<slot></slot>
<p class="placeholder">Nothing to show yet.</p>
</section>
`;
}
connectedCallback() {
const slot = this.shadowRoot.querySelector("slot");
slot.addEventListener("slotchange", () => {
const hasContent =
slot.assignedElements().length > 0;
this.classList.toggle("is-empty", !hasContent);
});
}
}
Shadow DOM does not prevent slotchange. The event
still fires on the slot inside the shadow tree when light DOM
content changes.
Reacting to Dynamic Slot Updates
const panel = document.querySelector("content-panel");
panel.innerHTML = `<p>First message</p>`;
// slotchange fires
panel.innerHTML = "";
// slotchange fires again
panel.innerHTML = `
<p>Updated message</p>
<button type="button">Dismiss</button>
`;
// slotchange fires again
Any change to assigned slotted content can trigger
slotchange, including initial render, replacement,
and removal of child nodes.
Run an Initial Slot Check
connectedCallback() {
this.shadowRoot.innerHTML = `
<section><slot></slot></section>
`;
this.slot = this.shadowRoot.querySelector("slot");
this.slot.addEventListener("slotchange", () => {
this.syncState();
});
// Important: slotchange may not fire on first connect
this.syncState();
}
Do not rely only on the event for initial rendering. Call your
slot-reading logic once during connectedCallback() so
the component state is correct before any later changes occur.
Accessibility Updates with slotchange
updateAccessibility(slot) {
const assigned = slot.assignedElements();
const host = slot.getRootNode().host;
if (assigned.length === 0) {
host.setAttribute("aria-hidden", "true");
return;
}
host.removeAttribute("aria-hidden");
const heading = assigned.find(
(node) => /^H[1-6]$/.test(node.tagName)
);
if (heading && !heading.id) {
heading.id = `${host.localName}-title`;
}
if (heading) {
host.setAttribute("aria-labelledby", heading.id);
}
}
Use slotchange to keep accessibility attributes in
sync when titles, descriptions, or actionable content appear or
disappear from slotted regions.
When slotchange Fires
| Action | Fires slotchange? |
| Add slotted child element | Yes |
| Remove slotted child element | Yes |
Change slot="name" assignment | Yes |
| Update text inside slotted element | No |
| Change attribute on slotted element | No |
| Initial connect with stable slotted content | Not guaranteed — run manual sync too |
slotchange vs MutationObserver
| Feature | slotchange | MutationObserver |
| Purpose | Slot assignment changes | Any DOM mutation |
| Complexity | Simple and slot-specific | More general and verbose |
| Best For | Empty states and slot-aware layout | Broader DOM watching needs |
| Performance | Focused on slot updates | Can fire for many unrelated changes |
When to Use slotchange
- You need empty-state behavior when no content is slotted.
- Layout changes depend on which slot received content.
- Accessibility labels must update when slotted titles change.
- You want to validate or count assigned slot content.
- Named slots need independent header, body, or footer logic.
- Dynamic consumer content is added after initial render.
Common slotchange Use Cases
- Cards that hide footer areas when no footer slot content exists.
- Panels that show placeholder text for empty content regions.
- Tabs that recalculate active content when slotted panels change.
- Dialogs that set
aria-labelledby from slotted titles. - Lists that count slotted items for badges or summaries.
- Layout shells that adjust spacing based on assigned sections.
slotchange Best Practices
- Attach listeners to the actual
<slot> elements. - Run an initial sync in
connectedCallback(). - Use
assignedElements() for element-based logic. - Handle each named slot separately when needed.
- Keep slotchange handlers focused and lightweight.
- Do not use slotchange for text or attribute edits inside slotted nodes.
- Remove listeners in
disconnectedCallback() when appropriate.
Common slotchange Mistakes
- Assuming slotchange always fires on first render.
- Listening on the host element instead of the slot.
- Using slotchange to detect text changes inside slotted content.
- Forgetting to handle multiple named slots independently.
- Not checking for empty assigned content before updating UI.
- Performing expensive work on every slotchange without need.
- Using MutationObserver when slotchange is the simpler tool.
Key Takeaways
slotchange fires when assigned slotted nodes change. - Use it to react to added, removed, or reassigned slot content.
assignedElements() and assignedNodes() read slot content. - Run an initial slot sync during component setup.
- slotchange is essential for dynamic, slot-aware Web Components.
Pro Tip
Combine slotchange with a one-time initial check in
connectedCallback(). That gives you reliable empty
states and slot-aware layout both on first render and after later
content updates.