When working with Lightning Web Components (LWC), you’ll often need your parent component to react when something happens deep inside a child or even a grandchild. For example, think of a table inside another table with checkboxes — when the user checks a box, the parent should know. This is where event bubbling comes in.
🔹 What is Event Bubbling?
Event bubbling means that when an event occurs on a child element, it automatically “bubbles up” through ancestor components unless stopped. In LWC, you can create custom events that bubble by configuring them like this:
new CustomEvent('eventname', {
bubbles: true,
composed: true,
detail: { ... }
});
bubbles: true→ allows the event to travel upward.composed: true→ allows it to cross the shadow DOM boundary.
🔹 Real Example: Nested Table with Checkbox
We’ll build a structure like this:
orderTable(parent)orderRow(middle)itemCheckbox(child with checkbox)
🟢 Step 1: Child Component (itemCheckbox)
<!-- itemCheckbox.html -->
<template>
<lightning-input type="checkbox" data-id="item-101"
label="Select Item"
onchange={handleChange}>
</lightning-input>
</template>
// itemCheckbox.js
import { LightningElement } from 'lwc';
export default class ItemCheckbox extends LightningElement {
handleChange(event) {
this.dispatchEvent(
new CustomEvent('itemselected', {
bubbles: true,
composed: true,
detail: {
checked: event.target.checked,
itemId: this.dataset.id
}
})
);
}
}
Here, the event name is itemselected.
🟢 Step 2: Middle Component (orderRow)
<!-- orderRow.html -->
<template>
<tr>
<td>{item.name}</td>
<td><c-item-checkbox></c-item-checkbox></td>
</tr>
</template>
By default, the event will bubble past this component. But sometimes we want to add more info, like a rowId, before it reaches the parent.
// orderRow.js
import { LightningElement, api } from 'lwc';
export default class OrderRow extends LightningElement {
@api item;
handleItemSelected(event) {
event.stopPropagation(); // stop original event
this.dispatchEvent(
new CustomEvent('rowselection', {
bubbles: true,
composed: true,
detail: {
rowId: this.item.id,
...event.detail
}
})
);
}
}
<!-- orderRow.html -->
<template>
<tr onitemselected={handleItemSelected}>
<td>{item.name}</td>
<td><c-item-checkbox></c-item-checkbox></td>
</tr>
</template>
🟢 Step 3: Parent Component (orderTable)
<!-- orderTable.html -->
<template>
<table class="slds-table">
<thead>
<tr><th>Item</th><th>Select</th></tr>
</thead>
<tbody onrowselection={handleRowSelection}>
<c-order-row item={item1}></c-order-row>
<c-order-row item={item2}></c-order-row>
</tbody>
</table>
</template>
// orderTable.js
import { LightningElement } from 'lwc';
export default class OrderTable extends LightningElement {
handleRowSelection(event) {
const { rowId, itemId, checked } = event.detail;
alert(`Row ${rowId} → Item ${itemId} is ${checked ? 'checked' : 'unchecked'}`);
}
}
🔹 What If No Listener Exists?
If the parent does not listen to the event (no onitemselected or onrowselection), nothing breaks. The event bubbles up and disappears silently. No error is thrown. This makes your components reusable — they can emit events and only the parents that care will handle them.
🔹 Debugging Bubbling Events
To confirm your events are firing, you can attach a debug listener in your parent’s connectedCallback:
connectedCallback() {
this.template.addEventListener('itemselected', (event) => {
console.log('DEBUG >> Bubble event detected:', event.detail);
});
}
Or even listen at the document level:
document.addEventListener('itemselected', (event) => {
console.log('GLOBAL DEBUG >>', event.detail);
});
✅ Summary
- Child components can fire events with
bubbles: trueandcomposed: true. - Events automatically bubble through ancestors unless stopped.
- Intermediate components can enrich the event before re-dispatching.
- No listener = no problem. The event is ignored silently.
- Use debug listeners to trace events during development.
With these patterns, you can build powerful nested LWC components that communicate cleanly up the hierarchy without tight coupling.

Leave a Reply