Skip to content

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.

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.

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.

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 listening
document.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.

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.

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.

OptionBenefitCost
CustomEvent + detail payloadOutgoing data has a clear structure, loosely coupled, multiple listeners can subscribe at once without the element knowing who’s listeningNo 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 furtherTightly coupled to one specific consumer, supports only one listener at a time, and the callback has to be set before the element uses it
  • Forgetting bubbles: true — dispatching a CustomEvent without setting bubbles means only the element that dispatched it ever sees the event; an ancestor or document will never see it at all.
  • Mutating e.detail from a listener — detail should 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 + detail convention for every custom element, so event payloads stay predictable and easy to test. (github/catalyst)

Shoelace / Web Awesome design system — a component like sl-select dispatches an sl-change event with detail describing the selected item, so consumers can read the change without querying into the shadow DOM themselves. (webawesome.com)

What property of `CustomEvent` carries the event payload?
Which option makes a CustomEvent travel up the DOM tree?
How do you dispatch a custom event from inside an element?