LEARNING OBJECTIVES ⌵
- Bind and handle the native
slotchangeevent onHTMLSlotElementinstances. - Understand when the browser fires
slotchangeduring component initialization and runtime mutations. - Differentiate between direct slot assignment changes and internal child node mutations.
- Build reactive Web Components that automatically update badges, headers, and layouts when consumers inject or remove nodes.
📖 The Mental Model & Story (Intuitive Foundation)
Imagine a high-traffic highway toll plaza with several automated RFID toll lanes (<slot> elements). Above each lane is an optical motion sensor.
Whenever a car drives into the lane or exits the lane, the optical sensor trips and sounds a chime: slotchange!
+-------------------------------------------------------------------------------+
| TOLL PLAZA SENSOR MODEL |
+-------------------------------------------------------------------------------+
| |
| [ Incoming Vehicle Added to Light DOM ] |
| │ |
| v |
| ================== LANE 1 (<slot name="express">) ==================== |
| 🚗 (Car enters lane) ───> [ 🔔 SENSOR TRIPS: slotchange event! ] |
| ====================================================================== |
| |
| Toll Booth Controller: |
| - Reads new lane count: slot.assignedElements().length |
| - Updates overhead digital billboard: "3 Vehicles in Queue" |
| |
+-------------------------------------------------------------------------------+
The component does not need to constantly poll or run expensive interval loops. Whenever the consumer application appends a new element, deletes a child, or switches an item's slot attribute, the browser triggers the slotchange event, allowing the custom element to recompute its internal state instantly.
Technical Deep Dive & Specifications
The slotchange Event Lifecycle
The slotchange event is dispatched on an HTMLSlotElement whenever its assigned nodes list changes.
Consumer Action
┌───────────────────────┼───────────────────────┐
│ │ │
appendChild(node) removeChild(node) node.slot = "new-name"
│ │ │
└───────────────────────┼───────────────────────┘
│
[ Re-evaluate Slot Assignment ]
│
Assigned Nodes Changed?
/ \
(Yes) (No - e.g. text edit)
/ \
Fire 'slotchange' Event No Event Fired
Event Specifications & Properties
| Event Property | Value | Description / Architectural Implication |
|---|---|---|
| Event Name | 'slotchange' |
Standard DOM Level 4 event type. |
| Target | HTMLSlotElement |
The <slot> whose assigned node collection was modified. |
bubbles |
true |
Bubbles up the Shadow DOM hierarchy (can be caught on Shadow Root). |
cancelable |
false |
Cannot be intercepted or cancelled via e.preventDefault(). |
composed |
false |
Does NOT cross the Shadow DOM boundary into the outer document. |
[!IMPORTANT] Because
composedisfalse, listeners outside the custom element cannot detectslotchangeon the document root. You must attach the listener inside the Shadow DOM to the slot element itself:this.shadowRoot.querySelector('slot').addEventListener('slotchange', ...).
What Triggers slotchange vs What Does NOT
| Action | Triggers slotchange? |
Explanation |
|---|---|---|
| Adding a new child to Light DOM | ✅ YES | Assigned nodes list grows. |
| Removing an existing child | ✅ YES | Assigned nodes list shrinks. |
Changing element.slot = "other" |
✅ YES | Node leaves current slot and joins another. |
| Initial page load with markup | ✅ YES | Fired during initial slot assignment. |
Changing an element's textContent |
❌ NO | The node reference did not change; only internal data mutated. |
| Changing an element's CSS class | ❌ NO | No change to the slot distribution list. |
💻 Interactive Code Playground
Starter Code
Line-by-Line Code Breakdown
- Line 30–39 (
<reactive-task-list>...): Initial Light DOM markup containing 2 task items. - Line 66 (
this.slotElement.addEventListener('slotchange', ...)): Registers the event listener on the internal<slot>element. - Line 73 (
const assignedItems = this.slotElement.assignedElements()): Returns an array containing only the projected HTML elements (filtering out whitespace text nodes). - Line 76 (
this.countBadge.textContent = ...): The badge instantly reflects the new count without requiring custom polling or coupling to external mutation code. - Line 101 (
taskList.appendChild(item)): Appending an element to the Light DOM causes the browser to fireslotchangesynchronously within the microtask queue.
Expected Browser Render Output
(Clicking [ ✕ ] deletes the element from Light DOM; slotchange fires and updates the badge to [ 1 Item ]. Removing all items displays the fallback: 🎉 All tasks completed! Great job. and updates badge to [ 0 Items ].)
Active Tasks [ 2 Items ]
------------------------------------------------------------------
Audit security headers [ ✕ ]
Configure CSP nonce [ ✕ ]🏋️ Hands-On Exercise
🎯 The Challenge: Build an Auto-Index Tab Bar Component
Instructions:
- Define a
<tab-container>custom element. - In the Shadow DOM, render a top tab navigation bar (
.tab-bar) and a<slot name="panel">. - Listen for the
slotchangeevent on the<slot name="panel">. - Whenever panels are added or removed in the Light DOM, dynamically inspect all assigned panels and regenerate tab buttons in the
.tab-barbased on each panel'sdata-titleattribute! - Clicking a tab button should show only that panel and hide the others.
🏁 Starter Code Sandbox
⚠️ Common Pitfalls
- Listening for
slotchangeon the Host Element: Writingthis.addEventListener('slotchange', ...)fails becauseslotchangehascomposed: falseand does not cross the Shadow boundary. Always attach the listener to the<slot>element directly insideshadowRoot. - Expecting
slotchangeon Deep Text Mutations: Changingdocument.querySelector('.todo-item span').textContent = 'New'will NOT fireslotchange. If you need to observe internal attribute or text mutations of assigned nodes, attach aMutationObserverto each assigned element. - Infinite Re-render Loops: If your
slotchangelistener mutates Light DOM children of the host, it will trigger anotherslotchange, causing an infinite recursion.
💡 Pro Tips
- Initial Hydration in
connectedCallback: In some browser edge cases, initial HTML markup might finish slot distribution before your constructor listener binds. Always invoke your sync handler once insideconnectedCallback(). - Event Delegation on Shadow Root: If you have multiple named slots, you can attach a single
this.shadowRoot.addEventListener('slotchange', (e) => { ... })listener on the root becausebubbles: truewithin the Shadow DOM!
📌 Key Takeaways
- The
slotchangeevent fires onHTMLSlotElementwhen its assigned node list changes. - It has
bubbles: truewithin the Shadow DOM, butcomposed: false(does not escape into document). - Adding, removing, or changing the
slotattribute of Light DOM nodes triggersslotchange. - Modifying internal attributes or text of an existing assigned node does NOT fire
slotchange. - Use
slot.assignedElements()inside the listener to calculate element counts and re-render dynamic navigation. - --