Custom Events
Custom elements communicate outward by dispatching events. The browser’s built-in CustomEvent constructor lets you attach an arbitrary payload — the detail object — so the parent can receive structured data without ever reaching inside the element. This page covers how to create, dispatch, and listen to custom events.
The CustomEvent constructor
Section titled “The CustomEvent constructor”A CustomEvent is created with a type string and an options object. The most important option is detail, which carries your payload:
var event = new CustomEvent('count-change', { detail: { count: 42 }, bubbles: true,});bubbles: true makes the event travel up through the DOM tree, so a parent or ancestor — not just the element itself — can listen for it. Without this flag the event only fires on the element it was dispatched from and goes no further.
Dispatching from inside the element
Section titled “Dispatching from inside the element”Call this.dispatchEvent(event) from anywhere inside your class to fire the event:
class MyCounter extends HTMLElement { connectedCallback() { this._count = 0; this.innerHTML = '<button>Count: 0</button>'; this.querySelector('button').addEventListener('click', () => { this._count++; this.querySelector('button').textContent = 'Count: ' + this._count; this.dispatchEvent(new CustomEvent('count-change', { detail: { count: this._count }, bubbles: true, })); }); }}customElements.define('my-counter', MyCounter);The element does not care who is listening. It just announces that something happened and provides the data. This is the key to keeping elements reusable.
Listening from outside
Section titled “Listening from outside”Any code that has a reference to the element — or any ancestor, because bubbles: true — can register a listener with the standard addEventListener API:
// Parent listeningdocument.querySelector('my-counter').addEventListener('count-change', (e) => { console.log('Count is now', e.detail.count);});You can also listen at document level when bubbles: true is set, which is convenient when you do not have a direct reference to the element.
Why events instead of callback props?
Section titled “Why events instead of callback props?”A callback-prop pattern — passing a function reference as a property — works in JavaScript, but it creates a tight dependency: the element must know that the consumer expects a function on a specific property name, and the consumer must ensure the function is set before the element uses it.
Events are loosely coupled by design. Any number of listeners can subscribe or unsubscribe at any time without the element knowing. The element’s public API stays predictable: it dispatches a named event with a documented detail shape, and nothing more.
Live demo
Section titled “Live demo”The click-counter element below tracks how many times its button has been clicked. Each click dispatches a counter-change event with { value: N } in the detail. The page listens at document level and shows the latest value in a log area.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
CustomEvent + detail payload | Outgoing data has a clear structure, loosely coupled, multiple listeners can subscribe at once without the element knowing who’s listening | No built-in type-checking — you rely on documenting the detail shape yourself, and consumers can misunderstand it without documentation |
| Callback prop (passing a function as a property) | Direct call-back, can take a return value from the caller and use it further | Tightly coupled to one specific consumer, supports only one listener at a time, and the callback has to be set before the element uses it |
Common mistakes
Section titled “Common mistakes”- Forgetting
bubbles: true— dispatching aCustomEventwithout settingbubblesmeans only the element that dispatched it ever sees the event; an ancestor ordocumentwill never see it at all. - Mutating
e.detailfrom a listener —detailshould be treated as an immutable snapshot of state at the moment of dispatch. Changing it from one listener unexpectedly affects any other listener receiving the same event. - Naming an event in camelCase, like
countChange— use lowercase, hyphenated names like standard DOM events, e.g.count-change, to avoid case-sensitivity issues in HTML attribute event handlers.
💡 Real-world examples
GitHub’s Catalyst framework — strictly uses the
CustomEvent+detailconvention for every custom element, so event payloads stay predictable and easy to test. (github/catalyst)Shoelace / Web Awesome design system — a component like
sl-selectdispatches ansl-changeevent withdetaildescribing the selected item, so consumers can read the change without querying into the shadow DOM themselves. (webawesome.com)