Skip to content

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.