skip to content

In the browser DOM, how do you attach a data payload to an event you dispatch yourself with CustomEvent, and how does a listener read that payload back?

level: juniorimportance: must knowfreq 68%

answer

  1. second constructor argument
  2. one reserved payload slot
  3. named detail, not payload
  4. null when you leave it out
  5. plain Event has no such member

basics

~20 s

Put the payload in the CustomEvent constructor's detail option — new CustomEvent('cart:add', { detail: { id, qty } }) — and dispatch it on a node with dispatchEvent. Listeners read it as event.detail. A plain Event has no detail.

solid answer

~40 s

`CustomEvent` exists precisely to carry application data. You build it with `new CustomEvent(type, init)` where `init.detail` is the single payload slot: `el.dispatchEvent(new CustomEvent('cart:add', { detail: { id: 42, qty: 2 } }))`. Any listener registered for that type on that node reads `event.detail` and gets the exact same object reference the dispatcher passed — nothing is cloned. `detail` defaults to `null` if you omit it, and it is a read-only accessor, so you must supply it at construction time rather than assigning it afterwards. The base `Event` constructor accepts no `detail` at all, so use plain `Event` only for signals with no payload. The legacy `document.createEvent('CustomEvent')` plus `initCustomEvent()` pair still works in browsers but is deprecated; new code should use the constructor.

go deeper

for a junior

Be able to write the two lines from memory: construct with new CustomEvent(type, { detail }), dispatch with element.dispatchEvent(ev), and read event.detail in the listener.

for a middle

Explain that detail is a read-only accessor fixed at construction, that listeners share one object reference with no cloning, and that the base Event constructor has no detail member at all.

for a senior

Show the judgment behind the payload: keep detail a small, stable contract rather than leaking internal state, and point out that a mutable shared reference lets one listener change what the next one sees.

for a principal

Own the convention across a codebase — namespaced event names, a single emit helper, typed detail contracts — so that consumers of a widget can depend on its events the way they depend on its function signatures.

## What CustomEvent is for The DOM event system was designed for events the browser generates: `click`, `input`, `submit`. Each of those has a specialised interface (`MouseEvent`, `InputEvent`, `SubmitEvent`) with typed properties. When *your* code wants to announce something — a cart item was added, a dialog was confirmed, a widget finished loading — there is no browser-defined interface for it. `CustomEvent` fills that gap: it is `Event` plus exactly one extra, application-owned property called `detail`. ## The constructor shape ```js const ev = new CustomEvent('cart:add', { detail: { id: 42, qty: 2 }, // your payload, default null bubbles: false, // default false cancelable: false, // default false composed: false // default false }); cartButton.dispatchEvent(ev); ``` The first argument is the event *type* — an arbitrary string. There is no registry of valid names; picking a namespaced name such as `cart:add` or `my-widget:ready` keeps you from colliding with a real DOM event name like `change`. The second argument is the init dictionary. `detail` is the only member `CustomEvent` adds; the rest come from `EventInit` and are shared with plain `Event`. ## Reading it in a listener ```js cartButton.addEventListener('cart:add', (event) => { console.log(event.type); // 'cart:add' console.log(event.detail.id); // 42 console.log(event.target); // cartButton }); ``` A listener receives the same event object the dispatcher constructed, so `event.detail` is the *same reference* — not a copy. Two consequences follow. First, there is no serialisation cost and no structured-clone restriction: you can put a function, a DOM node, a class instance, anything, in `detail`. Second, if one listener mutates `event.detail`, every later listener sees the mutation. That is occasionally useful as a deliberate collect-results channel, but as an accident it is a nasty coupling bug. ## detail is read-only `detail` is exposed as a getter on `CustomEvent.prototype`. Constructing an event and then assigning `ev.detail = {...}` does not work — in strict mode (which all ES modules are) it throws a `TypeError`, and in sloppy mode it silently does nothing. Whatever you want listeners to see must go into the init dictionary. ## Event versus CustomEvent `new Event('ready')` is entirely legitimate when the fact that the event happened *is* the whole message. `Event`'s init dictionary has no `detail` member, and passing one is simply ignored, which is a common source of an undefined-payload bug: ```js el.dispatchEvent(new Event('ready', { detail: 1 })); el.addEventListener('ready', e => console.log(e.detail)); // undefined ``` So: no payload → `Event`; payload → `CustomEvent`. ## Can I just set my own property? Event instances are ordinary JavaScript objects, so `ev.payload = data` does work. It is still worth avoiding. `detail` is the documented, tool-known slot: TypeScript's `CustomEvent<T>` types it, framework wrappers forward it, and any other developer reading the dispatch site knows immediately where to look. An ad-hoc expando is invisible to all of that. ## The legacy API Older code creates events with `document.createEvent('CustomEvent')` followed by `initCustomEvent(type, bubbles, cancelable, detail)`. Both are deprecated in the DOM standard but still implemented for compatibility. The only reason to recognise them is to read old code; write the constructor form. ## A practical shape A reusable helper keeps dispatch sites honest: ```js function emit(el, type, detail) { return el.dispatchEvent(new CustomEvent(type, { detail, bubbles: true })); } ``` That one line fixes the naming convention, guarantees `detail` is always populated, and makes the bubbling decision explicit in one place instead of at fifty call sites.

  • Is the detail object copied or cloned for each listener?
    Neither — every listener receives the very same object reference the dispatcher constructed. Nothing is serialised, so `detail` may hold DOM nodes, functions or class instances. The flip side is that a listener mutating `event.detail` changes what later listeners see, which couples handlers together in ways that are hard to trace.
  • Why not just set your own property on the event, like ev.payload = data?
    It does work, since events are ordinary objects. But `detail` is the standard, documented slot: TypeScript's `CustomEvent<T>` types it, tooling and wrapper layers forward it, and any reader knows where the payload lives. An ad-hoc expando is invisible to all of that for no benefit.
  • What happens if you pass detail to the plain Event constructor?
    It is ignored. `EventInit` defines only `bubbles`, `cancelable` and `composed`, so `new Event('ready', { detail: 1 })` produces an event whose `detail` is `undefined` in the listener. Use `Event` for payload-free signals and `CustomEvent` whenever you need to carry data.

Think of detail as the single labelled envelope slot on a standard postal form: the form itself is the event, and the platform guarantees exactly one place to put your contents — writing on the margins works, but nobody downstream is looking there.

saying these in an interview costs you the question

  • Thinks detail is deep-cloned or structured-cloned per listener
  • Assigns event.detail after construction and expects it to stick
  • Believes the plain Event constructor accepts a detail option
  • Reaches for document.createEvent and initCustomEvent in new code
  • Assumes detail must be JSON-serialisable

context