Slotchange and Assigned Nodes
The slotchange event
Section titled “The slotchange event”The slotchange event fires on a <slot> element whenever the set of assigned nodes changes. This includes the moment the element first connects to the DOM with children already present, when a child is added or removed from the host element, and when a child’s slot attribute changes.
Listen for it directly on the slot element:
const slot = shadow.querySelector('slot');slot.addEventListener('slotchange', () => { console.log('Assigned nodes changed');});assignedNodes and assignedElements
Section titled “assignedNodes and assignedElements”slot.assignedNodes() returns all nodes currently projected into that slot, including text nodes and comment nodes. slot.assignedElements() returns only Element nodes, which is usually what you want when iterating over projected children.
Both methods accept an options object with a flatten property. Passing { flatten: true } recursively expands any nested slot elements to include their own assigned nodes, which is useful when you have slots inside slots.
class MyList extends HTMLElement { connectedCallback() { const shadow = this.attachShadow({ mode: 'open' }); shadow.innerHTML = '<slot></slot>'; const slot = shadow.querySelector('slot'); slot.addEventListener('slotchange', () => { const nodes = slot.assignedElements(); console.log('Slot has', nodes.length, 'elements'); }); }}customElements.define('my-list', MyList);Practical use
Section titled “Practical use”Reacting to slotchange lets your component stay in sync with its projected children without polling or mutation observers. Common patterns include:
- Counting projected items and reflecting that count in the shadow DOM or as an attribute
- Validating that only expected element types are slotted in
- Synchronising
aria-labelor other accessibility attributes when the number of items changes - Wiring up event listeners to newly projected children
The key insight is that your component does not own the light DOM children — the consumer does. The slotchange event is the bridge that lets you react to those external changes.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
Listen to the slotchange event to sync state | The component reacts to content changes in real time with no polling | Requires writing and cleaning up an event listener, adding lifecycle complexity |
Use MutationObserver instead of slotchange | Detects finer-grained changes, like an attribute or text content changing inside a child | Higher overhead and more setup than slotchange, which was designed specifically for this use case |
Common mistakes
Section titled “Common mistakes”- Forgetting to listen for
slotchangewhen projected content changes dynamically — leaves the shadow DOM’s UI stuck on stale data, not updating as children are actually added or removed. - Using
slot.assignedNodes()and expecting only elements —assignedNodes()also returns text and comment nodes; useslot.assignedElements()if you only want elements. - Forgetting to pass
{ flatten: true }when a slot is nested inside another slot — you get an incomplete list of assigned nodes, since nodes inside a nested slot aren’t included by default.
💡 Real-world examples
Lit uses the pattern of listening to
slotchangetogether withassignedElements()in components that need to count or check the type of projected children, such as a tab list that needs to know how many tabs it has.Shoelace uses
slotchangeinside a component like<sl-select>to detect when the projected options change, then syncs the selected state to match.