completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Custom events

Create your own JavaScript events with CustomEvent, dispatch them with dispatchEvent, and decide when events are a better fit than callbacks.

By the end, you can
  • 01
    Send data with an eventMake a CustomEvent and put the data listeners need in detail.
  • 02
    Send and cancel eventsSay exactly when listeners run, and when dispatchEvent returns false.
  • 03
    Pick the right toolChoose between custom events, callbacks, promises, and EventTarget classes.

Make one part of your program announce what happened

An event is a message that says "something just happened". A listener is a function that waits for one kind of event and runs when it arrives. You already know built-in events, such as click and submit, from the earlier Events lessons. The browser sends those for you.

A custom event is an event you invent for your own app. You choose its name, such as cart:add, timer:done, or profile:saved. Your own code sends it.

Why is that useful? Say a product button adds tea to the cart. Three parts of the page care:

  • the cart badge, which shows how many items are in the cart,
  • the analytics logger, which counts what people buy,
  • the discount banner, which may show a deal.

The button could call all three parts itself. But then the button must know about every one of them. Instead, the button can announce, "an item was added". Each part that cares listens for that announcement and reacts. The button doesn't need to know who is listening.

Real-life analogyA school announcement

A teacher says over the school speakers, "Drama club meets in Room 4." The teacher doesn't know every student who cares. Students who listen can go to Room 4, write it down, or ignore it. A custom event works the same way in your code.

In real life: The teacher speaks into the microphone
In JavaScript: Calling dispatchEvent(...)
In real life: Students who chose to listen
In JavaScript: Functions added with addEventListener
In real life: The message the teacher reads out
In JavaScript: The data in detail
In real life: The hallway speaker plays it too
In JavaScript: bubbles: true lets the elements around the target hear it too
The main idea

A custom event is an event object with a name you made up for your app. Make it with new CustomEvent(name, options). Put any data the listeners need in detail. Send it with target.dispatchEvent(event). The target is the object you send the event from.

Send data with CustomEvent and detail

INTERACTIVE

You make a custom event with new CustomEvent(type, options). It takes two things:

  • type: the event's name, as a string, such as "cart:add".
  • options: an object with settings. You can leave it out.

The options object can have four settings. Each one has a job:

  • detail holds the data you want to send, such as which item was added. If you leave it out, it is null.
  • bubbles says whether the event travels up to the elements around the target. It is false unless you set it.
  • cancelable says whether a listener may cancel the event. It is false unless you set it.
  • composed is for Web Components; you'll see it near the end of the lesson. It is false unless you set it.

The plain Event class makes events too, with new Event(type, options). But it has no detail. So when listeners need data, use CustomEvent.

Make a cart:add event and read it backPop out in the code editor (opens in a new tab)JavaScript
const event = new CustomEvent("cart:add", {  detail: { id: "tea", name: "Mint tea", qty: 2 },  bubbles: true,  cancelable: true,}); console.log(event.type);console.log(event.detail.name);console.log(event.bubbles);console.log(event.cancelable);console.log(event.isTrusted);

Lines 1 to 5 make the event. Line 1 names it cart:add. Line 2 puts an object with the tea's id, name, and quantity in detail. Lines 3 and 4 turn on bubbles and cancelable.

Lines 7 to 11 print five things:

  • Line 7 prints the name, cart:add.
  • Line 8 reads inside detail and prints Mint tea.
  • Lines 9 and 10 print true and true, because lines 3 and 4 turned those settings on.
  • Line 11 prints false. isTrusted is true only for events that come from a real person, like a real click. Your code made this event, so it is false.
A name tag that says cart:add points to a cart called event. The cart holds a detail card. Reading event.detail.name gives Mint tea.typecart:addevent namedetail: {…}eventevent.detail.nameMint tea
The event has a name, cart:add, and carries a detail object. A listener reads event.detail.name and gets Mint tea.

Now leave the settings out. This example sets only detail. Then it prints the count and three settings.

Settings you leave out are falsePop out in the code editor (opens in a new tab)JavaScript
const event = new CustomEvent("cart:updated", {  detail: { count: 3 },}); console.log(event.detail.count);console.log(event.bubbles);console.log(event.cancelable);console.log(event.composed);

Line 5 prints 3, the count from detail. Lines 6, 7, and 8 print false, false, and false. So an event doesn't bubble, can't be cancelled, and doesn't leave a Web Component, unless you ask for it.

Other code listens for your event by its name. So pick a name that says exactly what happened, such as cart:item-added. The colon is just a habit that makes names easy to read; any string works. What matters is that the name is clear, doesn't sound like a browser event, and is written down for your team.

Give the event a clear namePop out in the code editor (opens in a new tab)JavaScript
const event = new CustomEvent("cart:item-added", {  detail: { name: "tea" },}); console.log(event.type);console.log(event.detail.name);

Line 1 names what happened in the app: an item was added to the cart. It is not a browser action like click. Line 2 puts the item's name in detail. Lines 5 and 6 print cart:item-added and tea. A listener can tell what happened and which item it was.

Events aren't only for buttons. Here a Cart class announces each change. extends EventTarget on line 1 gives the class the event methods; you'll learn more about that later in this lesson.

A cart announces cart:updatedPop out in the code editor (opens in a new tab)JavaScript
class Cart extends EventTarget {  items = [];   add(item) {    this.items.push(item);    this.dispatchEvent(new CustomEvent("cart:updated", {      detail: { count: this.items.length, item },    }));  }} const cart = new Cart();cart.addEventListener("cart:updated", (event) => {  console.log("items", event.detail.count, "last", event.detail.item);});cart.add("tea");

Line 5 adds the item to the list. Lines 6 to 8 send a cart:updated event. Its detail holds the new count and the item. Lines 13 to 15 add a listener. Line 16 adds tea, so the listener prints items 1 last tea.

detail doesn't make a copy. It holds the very value you pass in. If that value is an object, your code and every listener share the same object. So listeners should only read detail, not change it, unless sharing is what you want.

detail holds the same object, not a copyPop out in the code editor (opens in a new tab)JavaScript
const cart = { count: 1 };const event = new CustomEvent("cart:updated", { detail: cart }); event.detail.count = 2;console.log(cart.count);

Line 2 puts the cart object in detail. Line 4 changes count through event.detail. Line 5 prints 2: the original cart changed too, because it is the same object.

Now try it on a real page. The #shop element holds two product buttons and a cart badge. The buttons don't change the badge themselves. Each button sends a cart:add event with detail. A listener on #shop hears it and updates the badge and the log.

How can a listener on #shop hear an event sent from a button? Through bubbling. With bubbles: true, the event first reaches the button. Then it travels up to each element around the button, one by one. So #shop hears it too.

Left: with bubbles true, the cart:add event travels from the button up to #shop, so the shop listener hears it. Right: with bubbles false, the event stays on the button, so #shop hears nothing.bubbles: true#shopcart:addbuttonshop hears itbubbles: false#shopbuttonshop hears nothing
The button sits inside #shop, and the listener is on #shop. Only with bubbles: true does the event travel up from the button to reach it.

Use the two toggle buttons in the demo to turn bubbles off, or to add a listener on the button that cancels the event. Then click Add and watch the badge.

Cart badge that listens for cart:add
The button sends cart:add; #shop listensPop out in the code editor (opens in a new tab)JavaScript
const shop = document.querySelector("#shop");const badge = document.querySelector("#cart-count");let items = 0; shop.addEventListener("cart:add", (event) => {  items = items + event.detail.qty;  badge.textContent = String(items);  console.log("Added", event.detail.name);}); button.dispatchEvent(new CustomEvent("cart:add", {  detail: { id: "tea", name: "Mint tea", qty: 1 },  bubbles: true,  cancelable: true,}));
Live pagebubbles on
Try it yourself

Click an Add button. The buttons don't know who is listening.

The buttons send real CustomEvent objects on a real page. Turn bubbles off and #shop stops hearing them.

dispatchEvent runs the listeners right away

STEP THROUGH

target.dispatchEvent(event) sends the event from target. It runs the matching listeners synchronously. That means right now, one after another, in the order they were added. Only when they have all finished does the line after dispatchEvent run. There is no waiting, no timer, and no promise.

dispatchEvent also returns true or false. It is almost always true. It is false only when both of these are true:

  • the event was made with cancelable: true, and
  • at least one listener called event.preventDefault(), which means "please don't do this".

That gives listeners a way to say no before something happens. In this example, a todo list asks its listeners before it deletes item 3.

Ask the listeners before deletingPop out in the code editor (opens in a new tab)JavaScript
const todo = new EventTarget(); todo.addEventListener("before-delete", (event) => {  event.preventDefault();}); const ok = todo.dispatchEvent(new CustomEvent("before-delete", {  detail: { id: 3 },  cancelable: true,})); console.log(ok);

On line 4, the listener calls preventDefault(). Line 9 made the event cancelable, so that call counts. Line 7 stores false in ok, and line 12 prints it. The todo list can check ok and skip the delete.

preventDefault does nothing without cancelablePop out in the code editor (opens in a new tab)JavaScript
const event = new CustomEvent("before-save"); event.preventDefault();console.log(event.defaultPrevented);console.log(new EventTarget().dispatchEvent(event));

Line 1 makes an event without cancelable: true. Line 3 calls preventDefault(), but it is ignored. Line 4 prints false: defaultPrevented tells you whether the event was cancelled, and it wasn't. Line 5 sends the event from a new EventTarget, an object that can send events (more on it below). It prints true.

Now step through a full run. Two listeners write to a log array. Predict where their entries land in the log.

Step through dispatchEvent
Step 0 of 5Ready
Your turn: follow the blue line

Predict the order of the log. Then step through a real EventTarget run, recorded in Node. The listeners run during dispatchEvent, not later.

Running in
  1. script
Next: line 12
const target = new EventTarget();const log = []; target.addEventListener("status", (event) => {  log.push("listener A: " + event.detail);}); target.addEventListener("status", (event) => {  log.push("listener B");}); const ok = target.dispatchEvent(  new CustomEvent("status", { detail: "ready", cancelable: true }),);log.push("after dispatch: " + ok); console.log(log.join(" | "));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Change the second listener

Changing this starts a new recording. Predict what dispatchEvent returns before you step.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
The log in order: before dispatch, listener A: ready, listener B, after dispatch: true. The middle two entries are written by the listeners, during the dispatchEvent call.log, in orderbefore dispatchlistener A: readylistener Bafter dispatch: truedispatchEventruns A, then B,then returns
The two listener entries land in the middle of the log. They are written while dispatchEvent runs, before the next line.
What if a listener throws an error?

In browsers and in Node, an error thrown by a listener is reported, for example in the console. It is not thrown back out of dispatchEvent the usual way, and the other listeners still run. So don't use errors to send answers from a listener back to the sender.

Running listeners right away is useful. A button can ask "did any listener cancel this?" and get the answer on the next line. But it has a cost: a slow listener makes the sender wait. Keep listeners short. Move slow work into a timer, a promise, or another tool that runs later.

Give your own objects events with EventTarget

INTERACTIVE

EventTarget is the built-in class that gives an object three methods:

  • addEventListener, to start listening,
  • removeEventListener, to stop listening,
  • dispatchEvent, to send an event.

Every element on a page already has these methods. But EventTarget isn't only for page elements. Modern browsers and Node 22 let you make one with new EventTarget(), or extend it in your own class. Then a timer, a downloader, a game level, or a small data store can announce what happened. It doesn't need to know anything about the page.

Real-life analogySpeakers in any building

A school isn't the only building that can have speakers. A museum can put in its own, and visitors in every room hear the announcements. The museum doesn't need to know who is in each room.

In real life: A school already has speakers
In JavaScript: Page elements already have the event methods
In real life: A museum puts in its own speakers
In JavaScript: Your class can extend EventTarget
In real life: People in many rooms can listen
In JavaScript: Many separate listeners can sign up

For a small app, you can even use one plain EventTarget as an event bus. An event bus is one shared object that carries announcements between parts of the app. Each part listens only for the event names it needs.

One shared EventTarget passes the newsPop out in the code editor (opens in a new tab)JavaScript
const bus = new EventTarget(); bus.addEventListener("cart:updated", (event) => {  console.log("badge", event.detail.count);}); bus.dispatchEvent(new CustomEvent("cart:updated", {  detail: { count: 2 },}));

Line 1 makes the bus. Lines 3 to 5 add a badge listener for cart:updated. Lines 7 to 9 send that event with a count of 2, so the listener prints badge 2.

The bus keeps no history and has no methods of its own. It only passes the news from the sender to the listener. In a bigger app, a named service or a framework store makes it clearer which code owns what, and which code removes old listeners.

Now try a Timer class that extends EventTarget. Each tick sends a tick event with the count in detail. After the last tick, it sends a done event. Two listeners hear each tick: a display that shows the count, and a logger that writes a row.

A timer that sends events
A Timer class that sends tick and done eventsPop out in the code editor (opens in a new tab)JavaScript
class Timer extends EventTarget {  constructor(limit = 3) {    super();    this.count = 0;    this.limit = limit;  }   tick() {    this.count += 1;    this.dispatchEvent(new CustomEvent("tick", {      detail: { count: this.count, limit: this.limit },    }));    if (this.count === this.limit) {      this.dispatchEvent(new Event("done"));    }  }}
Timer objectstopped
0ticks heard by the display
  1. Timer ready.
Try it yourself

Two listeners hear each tick: the display shows the count, and the logger writes a row.

A plain JavaScript class can send events too. It is not a page element. It only has addEventListener, removeEventListener, and dispatchEvent.

Click Remove logger. The logger listener was added with a { signal } option. A signal comes from an AbortController. When the controller aborts, every listener added with its signal is removed. The display listener keeps working. This is often easier than keeping a copy of each listener function just so you can remove it later.

Events and callbacks compared

COMPARE

A callback is a function you hand to other code, so that code can call it later. A callback reaches exactly one function. An event reaches every listener that signed up for it.

Real-life analogyA radio show and a phone call

A radio station plays its show to anyone who tunes in. It doesn't know who is listening. A phone call is different: you were given one number, and you call that one person.

In real life: The radio station
In JavaScript: The code that sends the event
In real life: People tune in or tune out
In JavaScript: addEventListener / removeEventListener
In real life: A phone call to one person
In JavaScript: A callback function you were handed

So how do you choose? If many parts may care, send an event. If one caller needs one answer, use a callback or return a promise. A promise is an object that gives one result later. Both ways are useful. Pick the one where each part needs to know less about the others.

Progress with one callbackPop out in the code editor (opens in a new tab)JavaScript
function downloadWithCallback(onProgress) {  onProgress(25);  onProgress(50);  onProgress(100);} downloadWithCallback((percent) => {  console.log("bar", percent);});
Progress as an event with two listenersPop out in the code editor (opens in a new tab)JavaScript
const download = new EventTarget(); download.addEventListener("progress", (event) => {  console.log("bar", event.detail.percent);}); download.addEventListener("progress", (event) => {  console.log("analytics", event.detail.percent);}); for (const percent of [25, 50, 100]) {  download.dispatchEvent(new CustomEvent("progress", {    detail: { percent },  }));}

Both versions report download progress at 25, 50, and 100 percent. In the first, downloadWithCallback calls the one onProgress function it was given. It prints bar 25, bar 50, and bar 100.

In the second, the loop on lines 11 to 15 sends a progress event three times. Two listeners hear each one: the bar listener and the analytics listener. So each percent prints twice: bar 25, then analytics 25, and so on.

Left: downloadWithCallback calls one function, onProgress. Right: the download object dispatches a progress event, and two listeners, bar and analytics, both hear it.callbackdownloadWithCallbackonProgressone functioneventdownloadbaranalyticsevery listener
A callback reports to the one function it was given. An event reaches every listener that signed up: here the bar and the analytics listener.

The table compares the two. Each row asks one question; read across to compare the answers.

How callbacks and events differ
QuestionCallback or promiseEvent
Who gets the message?One function, or one caller waiting on a promise.Every listener added for that event name.
What is it best for?Getting one result, changing one step of a task, or reporting to one owner.Telling many unrelated parts that something happened.
Can it send an answer back?Yes. The function returns a value, or the promise gives one.Not really. The only answer is false, when a listener cancels with preventDefault().
What must the sender know?It calls a function it was given.It knows only the event name and its data.

Event or callback?

SORTER

Sort these real situations. For each card, ask: does one caller need a direct answer? Or do many unrelated parts react to the same news?

Event or callback?
  • Product cards, a cart badge, and an analytics panel all react when an item is added to the cart.
  • items.sort((a, b) => a.price - b.price)
  • A pop-up window tells the component around it that the user clicked Save.
  • One caller asks for a user profile and needs the final value, or the error.
  • A timer lets a display, a sound effect, and a logger all react to every tick.
  • names.map((name) => name.toUpperCase())
Try it yourself
0 of 6 correct

Drag each card to the pattern where the parts need to know the least about each other.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Where you will use custom events

Custom events help whenever a small feature should announce something without importing the rest of the app.

  • Reusable widgets: a date picker sends date-change when the user picks a date.
  • Web Components: a Web Component is a custom HTML element you build yourself. It can send a custom event to the page around it. A later lesson covers Web Components.
  • Plain modules: a downloader or timer class extends EventTarget.
  • Analytics: a feature sends cart:add. The analytics code listens, and the feature's code doesn't change.

A Web Component can keep its inner parts in a hidden area called a shadow DOM. Normally, an event from inside that area stays inside. The composed: true setting lets the event cross out of it. It is a separate setting from bubbles.

composed and bubbles are separate settingsPop out in the code editor (opens in a new tab)JavaScript
const event = new CustomEvent("profile:save", {  composed: true,}); console.log(event.composed);console.log(event.bubbles);

Line 2 turns on composed, so line 5 prints true. Line 6 prints false. The event still doesn't travel up to the elements around it, because bubbles is its own setting.

Don't fake a built-in click

Sending your own event named click is not a good way to make an element act as if it were clicked. To get a real click action from a link or button, call its built-in method, element.click(). Keep custom names, like cart:add, for things that happen in your own app.

Common mistakes and surprises

  • “Custom events run later.” No. dispatchEvent runs every listener before it returns.
  • “Every custom event bubbles.” No. bubbles is false unless you set it.
  • “Event and CustomEvent both have detail.” No. Only CustomEvent has it, so use CustomEvent to send data.
  • “dispatchEvent gives back what the listeners return.” No. It returns true or false. false means a listener cancelled the event.
  • “Events made by code count as real user actions.” No. Events your code makes have isTrusted set to false.
  • “A custom event should reuse a name like click or change.” No. Pick your own name, like cart:add, so no one mixes it up with a browser event.

The table lists each setting, its value when you leave it out, and why that matters.

What each setting is when you leave it out
SettingIf you leave it outWhy it matters
detailnullListeners get no data. To send data, use CustomEvent and set detail.
bubblesfalseThe event stays on its target. A container around the target won't hear it.
cancelablefalsepreventDefault() is ignored, so dispatchEvent still returns true.
composedfalseThe event can't leave a Web Component's shadow DOM. You'll use this with Web Components later.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict the order

Read the starter code. Predict the three lines it prints, in order.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const target = new EventTarget();
target.addEventListener("ping", () => console.log("listener"));
console.log("before");
target.dispatchEvent(new Event("ping"));
console.log("after");

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind the data

    A custom event carries some data. Which property do the listeners read to get it?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeCancelled or not?

      Predict the value that dispatchEvent returns: true or false.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const target = new EventTarget();
      target.addEventListener("save", (event) => event.preventDefault());
      const ok = target.dispatchEvent(new Event("save", { cancelable: true }));
      console.log(ok);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeA tiny EventTarget class

        Read the class. Predict what its listener prints.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        class Timer extends EventTarget {
          tick() {
            this.dispatchEvent(new CustomEvent("tick", { detail: { count: 1 } }));
          }
        }
        const timer = new Timer();
        timer.addEventListener("tick", (event) => console.log(event.detail.count));
        timer.tick();

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeChoose the simpler pattern

          A profile loader has one caller. That caller waits for one final user object. Which is simpler: an event or a promise?

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            7 QUESTIONS
            Custom events quiz · 7 questionsScore: first tries count
            1. Question 1 of 7Which line makes an event that carries data for its listeners?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const event = new CustomEvent("cart:add", { detail: { qty: 2 } });
              console.log(event.detail.qty);

              Choose an answer to see the explanation.

            3. Question 3 of 7What does this code print, in order?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const target = new EventTarget();
              target.addEventListener("ping", () => console.log("listener"));
              console.log("before");
              target.dispatchEvent(new Event("ping"));
              console.log("after");

              Choose an answer to see the explanation.

            4. Question 4 of 7When does dispatchEvent(event) return false?

              Choose an answer to see the explanation.

            5. Question 5 of 7Which is a good name for a custom event?

              Choose an answer to see the explanation.

            6. Question 6 of 7What does bubbles: true change for a custom event sent from a page element?

              Choose an answer to see the explanation.

            7. Question 7 of 7Why might a class extend EventTarget?

              Choose an answer to see the explanation.

            Key takeaways

            • CustomEvent can carry data in detail. A plain Event can't.
            • dispatchEvent runs every listener right away. It returns false only when the event is cancelable and a listener called preventDefault().
            • Give events clear names of your own. Turn on bubbles, cancelable, or composed only when you need them.
            • Your own classes can extend EventTarget. It isn't only for page elements.
            • An event tells many listeners that something happened. A callback or a promise gets one result for one caller.

            Custom events are named announcements that your code sends, so separate listeners can react to what happens in your app.

            Up next: Mouse, pointer & touch.

            CompleteFrontend Clear concepts. Working examples.