W3docs

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 as event.detail. This is the dedicated channel for custom data; native event properties are read-only.
  • bubbles — when true, the event travels up through ancestor elements after firing, so a listener on a parent (or document) can catch it. Defaults to false.
  • cancelable — when true, a listener may call event.preventDefault() to signal that the default action should be skipped. Defaults to false.

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():

  1. It runs synchronously — unlike setTimeout, the listeners execute immediately, before the line after dispatchEvent() runs.
  2. It returns a boolean: false if the event was cancelable and a listener called preventDefault(), otherwise true. 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 componentMessage event carrying a message in detail.
  • 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 a dataUpdated event with the new data in detail.
  • The UI listener reads event.detail.data and 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); // true

EventTarget 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.

Practice

Practice
Which statements about custom events in JavaScript are correct?
Which statements about custom events in JavaScript are correct?
Was this page helpful?