How to Use slotchange Event in Custom Elements
The slotchange event is a specialized Web Components API
feature that allows custom elements to detect and react whenever the
nodes distributed into their Shadow DOM <slot>
elements are added, removed, or replaced. By attaching an event listener
directly to a <slot>, developers can track Light DOM
mutations that affect the component’s rendered output, read the
currently assigned nodes, and execute responsive logic without having to
manually monitor the entire DOM tree with a
MutationObserver.
Understanding Light DOM and Slots
When building custom elements with Shadow DOM, markup placed inside
the custom tag by the user is known as the Light DOM.
To render this content within the component’s internal markup,
developers define insertion points using <slot>
elements in the Shadow DOM.
While the custom element controls the Shadow DOM, the consumer of the
component controls the Light DOM. Because of this boundary, standard
shadow-side logic cannot directly anticipate when external scripts or
user actions alter the slotted content. The slotchange
event bridges this gap.
How the slotchange
Event Works
The slotchange event fires directly on a
<slot> element whenever its assigned nodes list
changes. This includes:
- Inserting a new element or text node into the slot.
- Removing an existing element or text node from the slot.
- Swapping the default fallback content for assigned Light DOM content (and vice versa).
The event does not bubble by default outside of the shadow root, meaning it is typically listened to inside the custom element’s implementation.
Implementing
slotchange in a Custom Element
To handle slotted updates, find the target <slot>
within the shadow root and attach a slotchange event
listener. Within the callback, access the assigned nodes via the
assignedNodes() or assignedElements()
methods.
class CustomCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<div class="card-wrapper">
<slot name="header">Default Header</slot>
<div class="body">
<slot></slot>
</div>
</div>
`;
}
connectedCallback() {
const headerSlot = this.shadowRoot.querySelector('slot[name="header"]');
const defaultSlot = this.shadowRoot.querySelector('slot:not([name])');
headerSlot.addEventListener('slotchange', (event) => {
this.handleSlotChange(event.target);
});
defaultSlot.addEventListener('slotchange', (event) => {
this.handleSlotChange(event.target);
});
}
handleSlotChange(slot) {
const assignedElements = slot.assignedElements();
console.log(`Slot "${slot.name || 'default'}" updated:`, assignedElements);
// React to the change (e.g., toggle classes, calculate layouts, validate structure)
if (assignedElements.length === 0) {
this.classList.add('is-empty');
} else {
this.classList.remove('is-empty');
}
}
}
customElements.define('custom-card', CustomCard);Key Characteristics and Best Practices
1. Granular Node Inspection
When handling a slotchange, you can pass options to
retrieve assigned content: * slot.assignedNodes() returns
all nodes, including whitespace and text nodes. *
slot.assignedElements() filters the results to return only
HTML element nodes. * Passing { flatten: true } (e.g.,
slot.assignedElements({ flatten: true })) traverses nested
slots to return elements distributed from higher-level ancestors.
2. Mutation Limitations
The slotchange event only triggers when the
structure of the assigned nodes changes (elements added or
removed). It does not fire if an existing slotted
element simply updates an internal attribute, style, or text content
inside a child node. To monitor deep modifications within already
slotted elements, combine slotchange with a
MutationObserver targeted at the assigned nodes.
3. Initial Rendering
In most modern browsers, slotchange fires during initial
element creation if content is already assigned. However, to guarantee
reliable initialization, it is standard practice to manually call the
handler method once inside connectedCallback after querying
the slots.