Skip to content

Slotchange and Assigned Nodes

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');
});

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);

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-label or 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.

OptionBenefitCost
Listen to the slotchange event to sync stateThe component reacts to content changes in real time with no pollingRequires writing and cleaning up an event listener, adding lifecycle complexity
Use MutationObserver instead of slotchangeDetects finer-grained changes, like an attribute or text content changing inside a childHigher overhead and more setup than slotchange, which was designed specifically for this use case
  • Forgetting to listen for slotchange when 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; use slot.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 slotchange together with assignedElements() 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 slotchange inside a component like <sl-select> to detect when the projected options change, then syncs the selected state to match.

When does the slotchange event fire on a slot element?
What does slot.assignedElements() return compared to slot.assignedNodes()?
What does { flatten: true } do when passed to assignedNodes()?