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:

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.