Dispatching custom events

You already know how to listen for events. You attach a handler and wait for the browser to fire a click or a keydown. But the arrow points both ways: JavaScript can also create events and fire them itself. Nothing about a handler cares whether the click came from a real finger on a trackpad or from a line of code.

This unlocks two useful patterns. First, custom events let a component announce what’s happening inside it. A JavaScript menu might emit open when it expands, select when a user picks an item, and close when it collapses. Other code subscribes to those signals without reaching into the menu’s internals. Second, you can synthesize built-in events like click or mousedown, which is handy for automated tests that need to drive the UI as if a person were using it.

menu component
dispatchEvent(…)
— open →
— select →
— close →
listener code
addEventListener(…)
A component emits named events; unrelated code listens without knowing how the component works inside.

The Event constructor

Just as DOM elements come in a class hierarchy (HTMLElement, HTMLInputElement, and so on), events do too. Sitting at the root is the plain Event class, and everything else extends it.

You build a raw event object like this:

let event = new Event(type, options);

Two arguments:

  • type — a string naming the event. It can be a standard name like "click", or something you invented like "my-event".
  • options — an optional object with two flags:
    • bubbles: true/false — when true, the event travels up through ancestors after firing on the target.
    • cancelable: true/false — when true, a handler is allowed to cancel the default action with preventDefault(). We’ll see what that means for a custom event further down.

Leave options out and both flags default to false, exactly as if you passed {bubbles: false, cancelable: false}.

dispatchEvent: actually firing it

Constructing an event object does nothing on its own. It’s just an object sitting in memory. To make handlers react, you dispatch it onto an element:

elem.dispatchEvent(event);

From that point on, the event behaves like any browser-generated one. Handlers attached to elem run. If you created it with bubbles: true, it climbs to ancestors and their handlers run too.

Here a click is fired entirely from script. The onclick handler can’t tell the difference — it runs and shows the alert just as if you’d pressed the button:

<button id="elem" onclick="alert('Click!');">Autoclick</button>

<script>
  let event = new Event("click");
  elem.dispatchEvent(event);
</script>
user clicks button
──────→
isTrusted: true
elem.dispatchEvent(event)
──────→
isTrusted: false
onclick handler runs (either way)
Two ways to reach the same handler: a real click, and a scripted dispatch.

Try both paths below. The same click handler runs either way, but watch the isTrusted value flip depending on whether you clicked the target or the script dispatched onto it:

interactiveReal click vs scripted dispatch

Making an event bubble

Set bubbles: true and your event propagates up the tree like a native one. Below, a made-up "hello" event fires on an <h1>, yet we catch it on document because it bubbles all the way up:

<h1 id="elem">Hello from the script!</h1>

<script>
  // listen up on document...
  document.addEventListener("hello", function(event) { // (1)
    alert("Hello from " + event.target.tagName); // Hello from H1
  });

  // ...but fire down on elem!
  let event = new Event("hello", {bubbles: true}); // (2)
  elem.dispatchEvent(event);

  // the document handler fires and shows the message
</script>
document  ·  listener catches “hello”
▲ bubbles
<body>
▲ bubbles
<h1 id=“elem”>  ·  target, dispatch here
A custom 'hello' event fires on the h1 and bubbles up to document, where the listener catches it.

Two things to be careful about:

  1. Use addEventListener for custom events. The on<event> shortcut only exists for built-in event names. There’s no such thing as document.onhello — assigning to it does nothing, because the browser never wires up an onhello property for a name it doesn’t know.
  2. Bubbling is opt-in. Without bubbles: true, the event fires only on elem itself and never reaches document.

Beyond that, propagation works identically for custom and built-in events. The same capturing phase runs on the way down and the same bubbling phase runs on the way up, so a listener registered with &#123;capture: true&#125; still catches your "hello" on the way down.

MouseEvent, KeyboardEvent, and friends

The generic Event is fine for names the browser doesn’t recognize. But for standard UI events, there are dedicated constructors. A short slice of the UI Events specification:

  • UIEvent
  • FocusEvent
  • MouseEvent
  • WheelEvent
  • KeyboardEvent
  • …and more

When you want to create one of these, reach for its specific constructor — new MouseEvent("click") rather than new Event("click"). The payoff is that each specialized constructor understands the properties that event type is supposed to carry, so you can set them through the options object:

let event = new MouseEvent("click", {
  bubbles: true,
  cancelable: true,
  clientX: 100,
  clientY: 100
});

alert(event.clientX); // 100

The plain Event constructor has no idea what clientX means, so it quietly drops any property it doesn’t recognize:

let event = new Event("click", {
  bubbles: true,    // only bubbles and cancelable
  cancelable: true, // are understood by Event
  clientX: 100,
  clientY: 100
});

alert(event.clientX); // undefined — the unknown property was ignored
new MouseEvent(“click”, …)
bubbles: true
cancelable: true
clientX: 100
clientY: 100
new Event(“click”, …)
bubbles: true
cancelable: true
clientX: undefined ✗
clientY: undefined ✗
Same options object, two constructors. Only the typed constructor keeps clientX.

You can patch around it by assigning event.clientX = 100 after construction. It works, but you’re doing by hand what the right constructor does for you, and you lose the guarantee that the object is a genuine MouseEvent. Browser-generated events always come through the correct class, so match that when you can. The full property list for each type lives in the spec — for example, MouseEvent.

CustomEvent: carrying your own data

For event names you invented, like "hello", the right tool is new CustomEvent. Under the hood, CustomEvent is almost identical to Event, with one addition: a detail property in the options object where you stash any data you want to hand to listeners.

<h1 id="elem">Hello for Maya!</h1>

<script>
  // the extra data rides along to the handler
  elem.addEventListener("hello", function(event) {
    alert(event.detail.name);
  });

  elem.dispatchEvent(new CustomEvent("hello", {
    detail: { name: "Maya" }
  }));
</script>

detail accepts anything — a string, a number, a whole object. Technically you could bolt data onto a plain new Event object after creating it, the same way we patched clientX earlier. But detail gives you a dedicated slot that won’t collide with any standard event property the browser might add now or later, and reaching for CustomEvent signals to anyone reading the code that this is your own event, not a native one.

CustomEvent(“hello”, { detail: { name: “Maya” } })
type: “hello”
bubbles: false
cancelable: false
detail: { name: “Maya” }  ← your data
handler reads it as event.detail.name → “Maya”
CustomEvent adds a detail field for your payload; everything else matches Event.

Here a tiny “cart” emits an item-added event whenever you add something, packing the item name and the running total into detail. A separate listener — which knows nothing about the button — just reads event.detail and updates the receipt:

interactivePassing a payload through event.detail

Cancelling with event.preventDefault()

Many native events come with a default action: a link navigates, a text drag starts a selection, a form submit sends data. Your custom events obviously have no built-in browser behavior. But the code that fires a custom event might have its own follow-up action planned, and it can let listeners veto it.

Here’s the contract. When a handler calls event.preventDefault(), that’s a signal: don’t go through with whatever you were about to do. The dispatcher learns about the veto because elem.dispatchEvent(event) returns false when the default was prevented (and true otherwise). So the dispatching code just checks the return value and decides whether to continue.

dispatchEvent(event)
│ handler ran…

called preventDefault() →
returns false → skip the action

did not →
returns true → proceed

dispatchEvent returns false when a handler cancelled; the dispatcher branches on it.

A concrete example: a hiding robot mascot (pretend it’s a closing menu or a dismissible panel). The hide() function fires a "hide" event before doing anything, giving any interested handler a chance to stop it. If nobody cancels, the mascot disappears; if a handler calls preventDefault(), it stays:

<pre id="robot">
   .=====.
   | o o |
   |  ^  |
   | --- |
   '====='
</pre>
<button onclick="hide()">Hide()</button>

<script>
  function hide() {
    let event = new CustomEvent("hide", {
      cancelable: true // without this flag, preventDefault does nothing
    });
    if (!robot.dispatchEvent(event)) {
      alert('The action was prevented by a handler');
    } else {
      robot.hidden = true;
    }
  }

  robot.addEventListener('hide', function(event) {
    if (confirm("Call preventDefault?")) {
      event.preventDefault();
    }
  });
</script>

Below, a panel fires a cancelable "closing" event before it disappears. Toggle the veto and press Close: when the handler calls preventDefault(), dispatchEvent returns false and the panel stays. Untick it and the close goes through:

interactiveA handler can veto the default action

Nested events run synchronously

Normally the browser handles events in a queue, one after another. If it’s busy running an onclick handler and the mouse moves, the resulting mousemove waits its turn — its handlers run only after the onclick finishes.

Dispatching an event from inside another handler breaks that pattern. A dispatchEvent call is handled immediately, right where it appears. The browser pauses the current handler, fully processes the new event (including all its bubbling), and only then resumes the code that fired it.

Watch the order here. The menu-open event is dispatched between two alerts inside onclick:

<button id="menu">Menu (click me)</button>

<script>
  menu.onclick = function() {
    alert(1);

    menu.dispatchEvent(new CustomEvent("menu-open", {
      bubbles: true
    }));

    alert(2);
  };

  // fires between 1 and 2
  document.addEventListener('menu-open', () => alert('nested'));
</script>

The output is 1 → nested → 2. The nested menu-open, even though it’s caught way up on document, finishes entirely before alert(2) runs.

onclick starts
alert(1)  → shows 1
dispatchEvent(menu-open) — pauses here
document handler → shows nested
…back to onclick…
alert(2)  → shows 2
onclick ends
Synchronous dispatch: the outer handler pauses while the nested event runs to completion.

This isn’t unique to dispatchEvent. Any handler that calls a method which itself triggers events sees the same nesting — those events run synchronously, right in the middle of the current one.

Say you want the opposite. You’d like onclick to finish completely before menu-open is touched, keeping the two flows cleanly separate. Two options: move the dispatchEvent to the very end of onclick, or — cleaner and more robust — wrap it in a zero-delay setTimeout:

<button id="menu">Menu (click me)</button>

<script>
  menu.onclick = function() {
    alert(1);

    setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
      bubbles: true
    })));

    alert(2);
  };

  document.addEventListener('menu-open', () => alert('nested'));
</script>

Now dispatchEvent is deferred to a later macrotask. It won’t run until the current call stack — including all of menu.onclick — has emptied out. The two handlers no longer interleave.

The output flips to 1 → 2 → nested.

task 1 — the click
alert(1) → 1
schedule setTimeout →
alert(2) → 2
then →
task 2 — the timer
dispatch menu-open
document handler → nested
With setTimeout, the whole onclick finishes first; the nested event runs afterward as a separate task.

Flip the switch and click Run to watch the order change. The synchronous dispatch splices nested right between 1 and 2; wrapping it in setTimeout pushes it to a later task so the whole click finishes first:

interactiveSynchronous nesting vs deferred dispatch

Summary

To fire an event from code, build an event object first, then dispatch it.

  • The generic Event(name, options) constructor takes any event name plus an options object with two flags: bubbles: true to make it propagate upward, and cancelable: true to allow event.preventDefault().
  • Typed native constructors like MouseEvent and KeyboardEvent additionally accept the properties specific to their type, such as clientX for mouse events. The plain Event constructor ignores those.
  • For your own event names, use CustomEvent. Its extra detail option carries whatever data you want, and every handler reads it back as event.detail.
  • elem.dispatchEvent(event) runs the event. It returns false if a handler cancelled a cancelable event, giving the dispatcher a clean way to check for a veto.
  • Events dispatched from inside a handler are processed synchronously, nested within the current one. Wrap the dispatch in setTimeout when you want the flows kept apart.

One word of caution on synthesizing native events like click or keydown: do it sparingly. Firing a fake browser event to trigger a handler is usually a sign of tangled architecture. The legitimate uses are narrow:

  • As a last-resort hack to make a stubborn third-party library behave when it exposes no other hook.
  • For automated testing, where you script a “click” and check that the interface responds correctly.

Custom events with your own names are a different story. Emitting them to broadcast what’s happening inside your menus, sliders, and carousels is a clean, common design that keeps components loosely coupled.