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?
answer
- second constructor argument
- one reserved payload slot
- named detail, not payload
- null when you leave it out
- plain Event has no such member
basics
~20 sPut 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
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.
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.
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.
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