Composed Events
This lesson explains Composed Events with clear examples, use cases, and best practices so you can build reusable Web Components confidently.
What Are Composed Events?
Composed events are events that can cross the Shadow DOM boundary and reach
the outside page. They are very important when a Web Component needs to
communicate with parent elements, application code, or other components.
In Shadow DOM, events are normally protected inside the component. By using
composed: true, you allow the event to move outside the Shadow
DOM safely.
| Concept | Description |
| Composed Event | An event that can cross the Shadow DOM boundary |
composed: true | Allows the event to leave Shadow DOM |
bubbles: true | Allows the event to bubble upward through the DOM |
detail | Passes custom data with a CustomEvent |
| Main Benefit | Public communication from Web Components |
| Common Use | Buttons, forms, menus, tabs, dialogs, and design system components |
Basic Composed Event Example
class UserCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<button type="button">Select User</button>
`;
}
connectedCallback() {
const button = this.shadowRoot.querySelector("button");
button.addEventListener("click", () => {
this.dispatchEvent(
new CustomEvent("user-selected", {
detail: {
userId: 101,
name: "Ayaan"
},
bubbles: true,
composed: true
})
);
});
}
}
customElements.define("user-card", UserCard);
<user-card></user-card>
<script>
document.addEventListener("user-selected", (event) => {
console.log(event.detail.userId);
console.log(event.detail.name);
});
</script>
The custom event starts inside the component and reaches the outside
document because both bubbles and composed are
set to true.
How Composed Events Work
| Step | What Happens |
| 1. User interacts | User clicks a button inside Shadow DOM |
| 2. Component creates event | The component creates a CustomEvent |
| 3. Event bubbles | bubbles: true lets the event move upward |
| 4. Event crosses boundary | composed: true lets it leave Shadow DOM |
| 5. Parent listens | The outside page receives and handles the event |
Bubbles vs Composed
| Option | Purpose | Needed For |
bubbles: true | Moves event upward through parent nodes | Parent-level event handling |
composed: true | Allows event to cross Shadow DOM boundary | Communication outside Web Component |
cancelable: true | Allows outside code to cancel the event | Optional validation or blocking behavior |
Example Without Composed
this.dispatchEvent(
new CustomEvent("cart-add", {
detail: {
productId: 25
},
bubbles: true,
composed: false
})
);
In this example, the event bubbles inside the Shadow DOM but may not reach
the parent page because composed is set to
false.
Correct Example With Composed
this.dispatchEvent(
new CustomEvent("cart-add", {
detail: {
productId: 25,
quantity: 2
},
bubbles: true,
composed: true
})
);
This version is correct for public component events because the event can
bubble and cross the Shadow DOM boundary.
Real-Time Example: Add To Cart Component
class AddToCartButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<button type="button">Add To Cart</button>
`;
}
connectedCallback() {
this.shadowRoot.querySelector("button").addEventListener("click", () => {
this.dispatchEvent(
new CustomEvent("add-to-cart", {
detail: {
productId: this.getAttribute("product-id"),
productName: this.getAttribute("product-name")
},
bubbles: true,
composed: true
})
);
});
}
}
customElements.define("add-to-cart-button", AddToCartButton);
<add-to-cart-button
product-id="1001"
product-name="Laptop">
</add-to-cart-button>
<script>
document.addEventListener("add-to-cart", (event) => {
console.log("Product ID:", event.detail.productId);
console.log("Product Name:", event.detail.productName);
});
</script>
The parent page does not need to know about the internal button. It only
listens to the public add-to-cart event.
Event Retargeting
When an event crosses the Shadow DOM boundary, the browser may change the
visible event.target to the custom element instead of the
internal Shadow DOM element.
document.addEventListener("click", (event) => {
console.log(event.target);
});
This behavior is called event retargeting. It protects the internal DOM
structure of the Web Component.
Using composedPath()
document.addEventListener("click", (event) => {
console.log(event.composedPath());
});
The composedPath() method returns the full event path,
including how the event traveled through Shadow DOM and the outside page.
Creating Public Component Events
this.dispatchEvent(
new CustomEvent("tab-change", {
detail: {
activeTab: "settings",
index: 2
},
bubbles: true,
composed: true
})
);
Public component events should be named clearly and should expose useful
data through detail instead of exposing internal DOM elements.
Cancelable Composed Events
const event = new CustomEvent("before-close", {
detail: {
reason: "user-click"
},
bubbles: true,
composed: true,
cancelable: true
});
const allowed = this.dispatchEvent(event);
if (!allowed) {
return;
}
this.remove();
Use cancelable: true when outside code should be able to stop
a component action by calling event.preventDefault().
When to Use Composed Events
- Your component uses Shadow DOM.
- The parent page needs to listen for component actions.
- You are building reusable design system components.
- You need to send data from the component to application code.
- You want a clean public API instead of exposing internal elements.
- The event should work across frameworks like React, Angular, or Vue.
Common Composed Event Use Cases
- Add to cart button events.
- Form submit or validation events.
- Modal open and close events.
- Tab change events.
- Dropdown select events.
- Toast dismiss events.
- Design system component interactions.
Composed Events Best Practices
- Use lowercase kebab-case event names such as
user-selected. - Use
bubbles: true and composed: true for public events. - Pass useful data through
detail. - Do not expose private Shadow DOM elements in event data.
- Dispatch events from the host component when possible.
- Use
cancelable: true only when outside code can block the action. - Document all public custom events in component documentation.
Common Composed Event Mistakes
- Using only
bubbles: true and forgetting composed: true. - Dispatching events only from deeply nested internal elements.
- Using unclear event names like
change or click for custom behavior. - Putting too much data inside
detail. - Exposing private internal DOM nodes through event payloads.
- Expecting non-composed events to reach the outside document.
- Not documenting custom event names and payload structure.
Key Takeaways
- Composed events can cross the Shadow DOM boundary.
bubbles: true moves the event upward. composed: true allows the event to leave Shadow DOM. detail is used to pass custom event data. - Use composed events to build clean, reusable Web Component APIs.
Pro Tip
For public Web Component events, usually use both
bubbles: true and composed: true. This makes the
event easier to listen to from the parent page and from JavaScript
frameworks.