JavaScript slotchange Event and Dynamic Slots

This article provides a comprehensive overview of the slotchange event in JavaScript Web Components, explaining how it works and how developers can detect and respond to dynamic slot assignments within the Shadow DOM. Readers will learn the mechanics behind the event, how to inspect projected nodes, and best practices for managing dynamic content changes in custom elements.

What is the slotchange Event?

The slotchange event is a native DOM event that fires on an <slot> element inside a Shadow Root whenever the nodes assigned to that slot change. This occurs when elements in the light DOM that are projected into a slot are added, removed, or swapped.

It is important to note that slotchange only triggers when the direct assignment of nodes to the slot changes. It does not track internal attribute or text changes inside those assigned nodes.

How to Listen for slotchange

To react to changes in slot content, attach an event listener directly to the <slot> element inside your custom element’s Shadow DOM:

class CustomCard extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <div class="card">
        <slot name="header"></slot>
        <slot></slot>
      </div>
    `;

    const defaultSlot = shadow.querySelector('slot:not([name])');
    const headerSlot = shadow.querySelector('slot[name="header"]');

    defaultSlot.addEventListener('slotchange', (event) => {
      this.handleSlotChange(event.target);
    });

    headerSlot.addEventListener('slotchange', (event) => {
      this.handleSlotChange(event.target);
    });
  }

  handleSlotChange(slot) {
    const assignedElements = slot.assignedElements();
    console.log(`Slot '${slot.name || 'default'}' updated:`, assignedElements);
  }
}

customElements.define('custom-card', CustomCard);

Inspecting Assigned Nodes

When the slotchange event fires, JavaScript can query the current state of the slot using two primary methods:

  1. slot.assignedElements(options): Returns an array of only the assigned Element nodes, ignoring whitespace and text nodes.
  2. slot.assignedNodes(options): Returns an array of all assigned nodes, including text and comment nodes.

Both methods accept an optional configuration object: * { flatten: true }: If set to true, the method returns nodes assigned to nested slots or fallback content if no nodes are explicitly assigned.

const elements = slot.assignedElements({ flatten: true });
if (elements.length === 0) {
  console.log('No elements assigned; displaying fallback content.');
}

Reacting to Dynamic Content Updates

JavaScript reacts automatically to dynamic DOM manipulations performed on the light DOM. Whenever child elements are appended, removed, or have their slot attribute modified, the browser re-evaluates slot assignments and fires slotchange.

const card = document.querySelector('custom-card');

// Appending a new child triggers the default slot's slotchange event
const p = document.createElement('p');
p.textContent = 'Dynamic paragraph content';
card.appendChild(p);

// Changing the slot attribute moves the node and triggers slotchange on both slots
p.setAttribute('slot', 'header');

Tracking Deep Changes Inside Assigned Elements

Because slotchange only triggers when the assigned top-level nodes change, changes within the children of those assigned nodes will not fire the event. To monitor deeper changes, combine slotchange with a MutationObserver:

const slot = shadowRoot.querySelector('slot');
const observer = new MutationObserver((mutations) => {
  console.log('Deep content change detected within assigned nodes');
});

slot.addEventListener('slotchange', () => {
  // Disconnect existing observations
  observer.disconnect();

  // Observe all current assigned elements for child or character data changes
  slot.assignedElements().forEach((element) => {
    observer.observe(element, {
      childList: true,
      subtree: true,
      characterData: true
    });
  });
});

Using the slotchange event alongside standard DOM querying methods ensures your Web Components remain decoupled, reactive, and responsive to any consumer-driven updates.