JavaScript Dispatching Custom Events
Learn how to create and dispatch custom events in JavaScript with CustomEvent and dispatchEvent, pass data through detail.
Dispatching Custom Events in JavaScript
The browser fires built-in events like click, submit, and keydown automatically. But JavaScript also lets you create and fire your own events on any DOM node, then listen for them with the same addEventListener() you already use for native events.
This is the foundation of loosely coupled application design: one part of your code announces that something happened ("data loaded", "cart updated") without knowing or caring who is listening. This page covers how to create custom events with the CustomEvent constructor, attach data to them, dispatch them with dispatchEvent(), and handle the common gotchas.
Creating a custom event
Use the CustomEvent constructor. The first argument is the event type (the name you will later listen for); the second is an options object:
let event = new CustomEvent("myEvent", {
detail: { message: "This is a custom event!" },
bubbles: true,
cancelable: true
});The three options you will use most often are:
detail— any value (object, string, number) you want to attach to the event. The listener reads it back asevent.detail. This is the dedicated channel for custom data; native event properties are read-only.bubbles— whentrue, the event travels up through ancestor elements after firing, so a listener on a parent (ordocument) can catch it. Defaults tofalse.cancelable— whentrue, a listener may callevent.preventDefault()to signal that the default action should be skipped. Defaults tofalse.
CustomEvent vs. Event
There is also a plain Event constructor, but it cannot carry data — it has no detail. Always reach for CustomEvent when you need to pass information along with the event:
// No way to attach data here:
let bare = new Event("ping", { bubbles: true });
// Use CustomEvent to send a payload:
let withData = new CustomEvent("ping", {
bubbles: true,
detail: { at: Date.now() }
});Dispatching the event
A created event does nothing until you fire it on an element with dispatchEvent():
let target = document.getElementById("box");
target.dispatchEvent(event);Two things are worth knowing about dispatchEvent():
- It runs synchronously — unlike
setTimeout, the listeners execute immediately, before the line afterdispatchEvent()runs. - It returns a boolean:
falseif the event wascancelableand a listener calledpreventDefault(), otherwisetrue. This lets the code that dispatched the event react to a "veto":
let cancelled = !target.dispatchEvent(event);
if (cancelled) {
console.log("A listener prevented the default action.");
}You can dispatch on any element. Dispatching on document works because bubbling events reach it, but firing on the specific element that "owns" the event (and letting it bubble) keeps your listeners scoped and avoids accidental global handlers.
Listening for a custom event
There is no special API on the listening side — addEventListener() works exactly as it does for click. The data arrives on event.detail:
element.addEventListener("myEvent", function (event) {
console.log(event.detail.message); // "This is a custom event!"
});Practical examples
Example 1: Communicating between components
Suppose two independent parts of a page need to talk without holding direct references to each other. One dispatches a custom event; the other listens. Because the event bubbles, the listener can sit on document:
<button id="sender">Send Message</button>// Listener in another component
document.addEventListener('componentMessage', function(event) {
alert('Received message: ' + event.detail.message);
});
document.getElementById('sender').addEventListener('click', function() {
// Create and dispatch the custom event
let customEvent = new CustomEvent('componentMessage', {
detail: { message: 'Hello from another component!' },
bubbles: true,
cancelable: true
});
document.dispatchEvent(customEvent);
});How it works:
- Clicking the button creates and dispatches a
componentMessageevent carrying a message indetail. - A listener elsewhere catches the event and reacts to it. Neither side imports the other — the event name is the only contract.
Example 2: Updating the UI after a data change
Custom events let you separate data logic from rendering logic. The data layer announces dataUpdated; the UI layer listens and re-renders. Here the update is triggered after a short delay to simulate an async fetch:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Custom Event UI Update Example</title>
</head>
<body>
<h1>User Status</h1>
<div id="userInfo">Loading user information...</div>
<script>
// Function to simulate a data update
function updateData() {
let dataUpdateEvent = new CustomEvent('dataUpdated', {
detail: { data: { username: 'user123', status: 'active' } }
});
document.dispatchEvent(dataUpdateEvent);
}
// UI component listening for data updates
document.addEventListener('dataUpdated', function(event) {
let userData = event.detail.data;
document.getElementById('userInfo').innerHTML =
`Username: <strong>${userData.username}</strong>, Status: <strong>${userData.status}</strong>`;
});
// Trigger the update after 2 seconds
setTimeout(updateData, 2000);
</script>
</body>
</html>How it works:
- After two seconds,
updateData()dispatches adataUpdatedevent with the new data indetail. - The UI listener reads
event.detail.dataand updates the page. The function that produced the data never touches the DOM directly.
Canceling a custom event
When an event is cancelable, a listener can call preventDefault(). The dispatcher then sees a false return value from dispatchEvent() and can skip its default behavior — the same pattern the browser uses for form submission or link clicks. This snippet runs in Node and demonstrates the synchronous flow:
let target = new EventTarget();
target.addEventListener("save", (event) => {
// Veto the save
event.preventDefault();
});
let event = new CustomEvent("save", { cancelable: true });
let notCancelled = target.dispatchEvent(event);
console.log(notCancelled); // false
console.log(event.defaultPrevented); // trueEventTarget is the base interface that DOM nodes inherit from, so the same logic applies whether you dispatch on an element in the browser or a standalone EventTarget.
Conclusion
Custom events give you a clean, framework-free way to wire parts of an application together. Create them with CustomEvent, attach a payload via detail, fire them with dispatchEvent(), and listen with the familiar addEventListener(). Set bubbles: true when a parent should catch the event, and cancelable: true when the dispatcher needs to honor a listener's veto. The result is code where modules communicate through named events instead of direct references — easier to test, extend, and maintain.
To go further, see how events travel through the DOM in bubbling and capturing, how listeners are attached in event handling in the DOM, and how preventDefault() affects browser default actions.