Handle the internal button’s click, then dispatch a named CustomEvent from the component host. The consuming page listens on that host and reads event.detail. This guide builds a complete local browser demo on a Mac and explains which event options each listener needs.
Define the notification your consumer needs
For a plain JavaScript web component, the consumer is the page or enclosing component that uses your custom element. Here, the concrete task is to let that consumer react when someone activates an internal button, without querying the component’s shadow DOM.
The example uses a synthetic tile labeled Tile A. Its public event is tile-activate, carrying an itemId and an activation count. CustomEvent provides the detail field for this application data; the event name is case-sensitive. Both the dispatching code and the listener must use exactly the same name. CustomEvent constructor documentation.
Treat those choices as a small interface contract:
- Name:
tile-activate. - Meaning: the internal button has been activated and the count updated.
- Payload:
detail.itemIdidentifies the tile;detail.countcontains its updated count. - Origin: the custom element host dispatches the notification.
- Delivery: direct consumers and ancestor listeners can receive it, including across enclosing shadow boundaries.
These are design choices for this example. A useful event describes the action the consumer cares about. That lets the component change its internal markup later while retaining the same notification interface.
Build and open the complete example on your Mac
You need a current desktop browser and a plain-text editor. No package installation or build step is required for this single-file example. It uses an autonomous custom element extending HTMLElement, the platform pattern documented in the HTML Standard’s custom elements section.
In TextEdit, choose File > New, then Format > Make Plain Text. Paste the complete document below. Choose File > Save, name it component-click.html, and select Use .html if prompted. These are Apple’s documented steps for creating an HTML file in TextEdit. Work with HTML documents in TextEdit on Mac.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Component click notification</title>
</head>
<body>
<h1>Component click notification</h1>
<section id="demo">
<action-tile id="tile" item-id="tile-a"></action-tile>
<p id="direct-status">Direct listener: waiting</p>
<p id="parent-status">Parent listener: waiting</p>
</section>
<script>
class ActionTile extends HTMLElement {
constructor() {
super();
this._count = 0;
this._button = null;
this._root = this.attachShadow({ mode: "open" });
this._handleClick = () => {
this._count += 1;
this.dispatchEvent(new CustomEvent("tile-activate", {
detail: {
itemId: this.getAttribute("item-id"),
count: this._count
},
bubbles: true,
composed: true
}));
};
}
connectedCallback() {
if (!this._button) {
this._button = document.createElement("button");
this._button.type = "button";
this._button.textContent = "Activate Tile A";
this._root.append(this._button);
}
this._button.addEventListener("click", this._handleClick);
}
disconnectedCallback() {
this._button.removeEventListener("click", this._handleClick);
}
}
customElements.define("action-tile", ActionTile);
const tile = document.getElementById("tile");
const demo = document.getElementById("demo");
const directStatus = document.getElementById("direct-status");
const parentStatus = document.getElementById("parent-status");
tile.addEventListener("tile-activate", (event) => {
const { itemId, count } = event.detail;
directStatus.textContent =
`Direct listener: ${itemId}, activation ${count}`;
});
demo.addEventListener("tile-activate", (event) => {
const { itemId, count } = event.detail;
parentStatus.textContent =
`Parent listener: ${itemId}, activation ${count}`;
});
</script>
</body>
</html>
Open the saved file in your browser and click Activate Tile A. The expected first result is Direct listener: tile-a, activation 1 and Parent listener: tile-a, activation 1. Each subsequent activation increases both displayed counts by one. These are expected results from the example, rather than reported browser test results.
The important sequence is visible in the code: update component state, create the notification, dispatch it from this, and let the consumer update its own interface. dispatchEvent() invokes applicable listeners synchronously before returning, so consumers see the updated count during their handlers. dispatchEvent documentation.
Choose the right bubbling and shadow-boundary options
The dispatch target and the listener’s location determine which options matter. A listener attached directly to the host receives an event dispatched on that host even when bubbles is false. An ordinary ancestor listener needs bubbling. Crossing a shadow boundary also requires composed. The underlying defaults for bubbles, composed, and cancelable are all false. DOM Standard: Event interface.
The following decision table is derived from those propagation rules. It assumes ordinary listeners registered without capture and no handler stopping propagation.
| Where the custom event starts | Where the consumer listens | Configuration for that route |
|---|---|---|
| Component host | Same host | Neither flag needs to be true |
| Component host | Ordinary ancestor in the same tree | bubbles: true |
| Internal shadow button | Ordinary ancestor outside that shadow root | bubbles: true, composed: true |
| Component host inside another shadow tree | Ordinary ancestor outside the enclosing shadow root | bubbles: true, composed: true |
In the demo, this.dispatchEvent(...) starts the notification on the host. It therefore does not need to escape the component’s own shadow root. composed: true makes the notification usable across an enclosing component’s shadow boundary if this tile is nested later. That broader reach should be a deliberate part of the public interface. Event composed documentation.
For a focused experiment, change only bubbles: true to bubbles: false, save, and reload the page. On activation, the direct status should update while the parent status remains Parent listener: waiting. Restore bubbling afterward. Changing only composed to false should leave both listeners working in this particular document because neither route crosses a shadow boundary.
Preserve initialization and reconnection behavior
Event delivery can appear correct initially yet produce duplicate notifications after a component moves or reconnects. Keep creation, connection, and cleanup responsibilities explicit.
The constructor calls super() first, initializes state, and attaches a shadow root. Rendering happens in connectedCallback(), guarded by the existing button reference so it runs once. These choices follow the HTML Standard’s constructor requirements and its guidance to guard initialization that must not repeat when connection callbacks run again. Requirements for custom element constructors and reactions.
The component registers its internal click listener when connected and removes it when disconnected. On reconnection, it reuses the existing button and preserves _count. Connection and disconnection callbacks are lifecycle hooks invoked as elements enter and leave the document. Using custom elements.
The stored _handleClick function matters: removal must identify the same function used during registration, with the same capture setting. Creating a fresh arrow function in the removal call would not identify the registered listener. removeEventListener documentation.
If you add listeners to window, document, or another external target in a larger component, give those listeners equally explicit cleanup. Keep lifecycle changes separate from the notification’s meaning so reconnecting the element does not accidentally announce a user action.
Diagnose missing or repeated notifications
Use the two visible status lines to locate the failure before changing event flags. They distinguish a problem with the component’s notification from a problem with ancestor delivery.
- Neither status changes: compare the event name at dispatch and registration, then check that the custom element definition and button handler execute. In this document, the consumer listeners are registered before any user activation.
- Only the direct status changes: check
bubbles. Also inspect any intermediate handler that stops propagation. - Delivery fails only when nested in another component: identify the shadow boundary on that route and check
composed. - Counts jump after reconnection: inspect repeated listener registration, especially new anonymous functions created each time the element connects.
Repeated anonymous functions are separate listener objects, even if their source text is identical. Keeping a stable function reference makes registration and cleanup easier to reason about. addEventListener() also distinguishes the listener’s capture option, which affects when it runs. addEventListener documentation.
A native click may already reach outside a shadow tree because browser-dispatched clicks are composed. A separate custom event is useful when the consumer needs a named component action and a defined payload. Avoid handling both notifications as the same application action unless that duplication is intentional. Event composed documentation.
Finally, keep notification and permission to proceed distinct in your event design. This example increments the count before dispatch and leaves the event noncancelable. A consumer calling preventDefault() cannot undo that update. A cancelable request would require an explicit implementation that checks the return value of dispatchEvent() before performing the requested action. dispatchEvent return value.
For this task, the complete contract is small: one internal button handler updates state, one named custom event leaves the host, and consumers read its documented payload. Keep the direct listener working first, then enable the propagation routes your containing interface actually needs.
Sources
- HTML Standard — Custom elements — WHATWG.
- DOM Standard — WHATWG.
- CustomEvent: CustomEvent() constructor — MDN Web Docs.
- EventTarget: dispatchEvent() method — MDN Web Docs.
- Event: composed property — MDN Web Docs.
- Using custom elements — MDN Web Docs.
- EventTarget: addEventListener() method — MDN Web Docs.
- EventTarget: removeEventListener() method — MDN Web Docs.
- Work with HTML documents in TextEdit on Mac — Apple.