skip to content

DOM and Events

You will learn how the browser models a document as a live tree of nodes and how events travel through that tree. This is the oldest and most reliably asked part of frontend interviewing, because every framework's virtual DOM and synthetic event system is a wrapper over these exact rules.

on this pageshow

questions

page 1 of 2

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

open as a page

In a DOM click listener registered on a <ul> element, what is the difference between event.target and event.currentTarget, and which of the two changes as the event travels?

level: juniorimportance: must knowfreq 76%

basics

~20 s

event.target is the deepest node the event was dispatched on and is identical for every listener in the path. event.currentTarget is the node whose listener is executing right now, so it differs from listener to listener.

open as a page

In the browser DOM, what can element.addEventListener('click', fn) do that assigning element.onclick = fn cannot?

level: juniorimportance: must knowfreq 68%

basics

~10 s

addEventListener registers any number of independent listeners on one element and accepts an options argument — capture, once, passive, signal. element.onclick is a single property slot, so each assignment silently replaces the previous handler.

open as a page

In the browser DOM, what is the difference between document.getElementById('user-menu') and document.querySelector('#user-menu'), and when does the selector form fail on an id that getElementById finds?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Both return the first matching element or null, but getElementById takes a raw id string and exists only on Document, while querySelector parses a CSS selector, so an id such as 3d-view needs CSS.escape or an attribute selector.

open as a page

In the browser DOM, what is the difference between a Node and an Element, and which kinds of node besides elements does a parsed HTML document contain?

level: juniorimportance: must knowfreq 55%

basics

~20 s

Node is the base type for everything in the DOM tree; Element is the subtype for tags. A parsed HTML document also contains Text nodes (including whitespace between tags), Comment nodes, the Document itself, and its DocumentType node.

open as a page

In DOM event handling, what does event.preventDefault() do that event.stopPropagation() does not, and what does event.defaultPrevented tell a listener that runs later in the path?

level: middleimportance: must knowfreq 68%

basics

~20 s

preventDefault() cancels the browser's built-in reaction, such as following a link or submitting a form, while the event keeps travelling. stopPropagation() stops the event reaching other nodes but leaves the default action intact. defaultPrevented reports whether cancellation already happened.

open as a page

After a DOM event is dispatched on an element, what three phases does it pass through, and in what order do capture-phase and bubble-phase listeners on its ancestors run?

level: middleimportance: must knowfreq 70%

basics

~20 s

An event first travels down from window to the target's parent firing capture listeners, then fires at the target, then travels back up firing bubble listeners. Ancestor capture handlers run outermost-first; ancestor bubble handlers run innermost-first.

open as a page

In the browser DOM, why does el.removeEventListener('click', () => save()) fail to remove a listener added with el.addEventListener('click', () => save()), and what exactly has to match for removal to succeed?

level: middleimportance: must knowfreq 66%

basics

~20 s

removeEventListener matches on three things: the event type, the identical callback object, and the same capture flag. Two arrow functions with identical bodies are different objects, so nothing matches — and removeEventListener reports nothing when it removes nothing.

open as a page

You are building 500 list items in plain JavaScript. What does a DocumentFragment give you over appending each item to the live list, and what state is the fragment in after you insert it?

level: middleimportance: must knowfreq 64%

basics

~20 s

A DocumentFragment is a parentless, never-rendered container you fill offscreen, so the live document is touched once instead of 500 times. Inserting the fragment inserts its children, not the fragment itself, which leaves the fragment empty afterwards.

open as a page

In the browser DOM, you take an element that is already in the page and append it to a different container. Do you end up with one node or two, and what happens to the listeners already attached to it?

level: middleimportance: must knowfreq 58%

basics

~20 s

One node. Inserting a node that already has a parent detaches it from that parent first, so insertion moves rather than copies. The node keeps its identity, so listeners added with addEventListener, dataset values and expando properties all travel with it.

open as a page

In a KeyboardEvent, what is the difference between event.key and event.code, and which one should a keyboard shortcut match against?

level: middleimportance: must knowfreq 62%

basics

~20 s

KeyboardEvent.key is the layout-aware value produced by the keystroke, such as "a", "A" or "Enter"; KeyboardEvent.code is the physical key position, such as "KeyA" or "Space", and never changes with layout. Match text shortcuts on key and positional controls on code.

open as a page

In the browser DOM, what does Element.closest('.row') return, and how does it differ from Element.matches('.row')?

level: middleimportance: must knowfreq 58%

basics

~10 s

Element.closest('.row') walks up from the element itself through its ancestors and returns the nearest element matching the selector, or null. Element.matches('.row') only tests that one element and returns a boolean.

open as a page

In the browser DOM, what is the difference between the live HTMLCollection returned by document.getElementsByClassName() and the static NodeList returned by document.querySelectorAll(), and how does that difference break a loop that removes elements?

level: middleimportance: must knowfreq 65%

basics

~20 s

getElementsByClassName returns a live HTMLCollection that keeps re-reflecting the document, while querySelectorAll returns a static NodeList captured once. Looping forward over the live collection while removing elements shifts the indexes and skips every other match.

open as a page

In the browser DOM, how does parent.append() differ from parent.appendChild(), and what do the newer methods prepend, before, after, replaceWith and remove let you do that the older Node methods cannot?

level: juniorimportance: should knowfreq 62%

basics

~20 s

append() accepts any number of nodes or strings, turns strings into text nodes, and returns undefined; appendChild() takes exactly one Node and returns it. prepend, before, after, replaceWith and remove share append()'s flexibility and act relative to the element itself, so no parent reference is needed.

open as a page

In a browser MouseEvent, what do screenX, clientX, pageX and offsetX each measure, and how do you reliably get the pointer position relative to a specific element?

level: juniorimportance: should knowfreq 55%

basics

~20 s

They differ only in origin: screenX is measured from the screen, clientX from the viewport, pageX from the document (clientX plus scroll offset), and offsetX from the padding edge of the event target. For element-local coordinates, subtract getBoundingClientRect().left from clientX.

open as a page

A widget calls child.dispatchEvent(new CustomEvent('item-selected')), and a listener on the child fires, but an identical listener on the parent container never does. Why, and what has to change?

level: middleimportance: should knowfreq 60%

basics

~10 s

CustomEvent's bubbles option defaults to false, so the event is delivered only to listeners on the dispatch target itself. Construct it as new CustomEvent('item-selected', { bubbles: true }) for ancestors to receive it.

open as a page

When you call el.dispatchEvent(new CustomEvent('save')), have the listeners already run by the time the next line of your function executes, and what does dispatchEvent return?

level: middleimportance: should knowfreq 46%

basics

~10 s

Dispatch is fully synchronous: dispatchEvent invokes every matching listener and only then returns, before the next statement runs. It returns false when the event was cancelable and a listener called preventDefault, and true otherwise.

open as a page

How do the once and signal options of addEventListener remove DOM listeners for you, and when would you choose signal over calling removeEventListener yourself?

level: middleimportance: should knowfreq 42%

basics

~20 s

once: true makes the browser remove the listener as it invokes the callback the first time. signal takes an AbortSignal; aborting its controller removes every listener registered with that signal, across any number of targets, without you holding a reference to each callback.

open as a page

In the browser DOM, what is the difference between node.cloneNode(true) and node.cloneNode(false), and what state does cloning fail to carry over to the copy?

level: middleimportance: should knowfreq 54%

basics

~20 s

cloneNode(true) copies the node and its whole subtree; cloneNode(false) copies only the node itself with its attributes. Either way the copy is detached and carries no listeners added with addEventListener and no custom JavaScript properties.

open as a page

Given `<div id="box">old</div>`, where do insertAdjacentHTML's four position strings put the new markup, and why would you use it rather than `box.innerHTML += markup`?

level: middleimportance: should knowfreq 45%

basics

~10 s

The four positions are beforebegin (previous sibling), afterbegin (first child), beforeend (last child) and afterend (next sibling). Unlike appending to innerHTML, it parses only the new string and leaves the element's existing nodes untouched.

open as a page

A touchmove listener added on document calls event.preventDefault() to stop the page scrolling, but the page scrolls anyway and the console warns that preventDefault cannot be used inside a passive listener. Why, and what are the options?

level: middleimportance: should knowfreq 42%

basics

~20 s

Browsers make touchstart, touchmove and wheel listeners passive by default when they are registered on window, document, document.documentElement or document.body, so preventDefault() is ignored. Opt back in with { passive: false }, or block the gesture declaratively with CSS touch-action.

open as a page

A card in a web page registers handlers for both touchstart and mousedown; on a phone the action runs twice. Why does that happen, and how do Pointer Events avoid the problem?

level: middleimportance: should knowfreq 48%

basics

~20 s

Touchscreen browsers fire the touch sequence and then replay it as compatibility mouse events so older mouse-only pages keep working, so both handlers run. Pointer Events replace both families with one stream — pointerdown, pointermove, pointerup — tagged by pointerType.

open as a page

When you call widget.querySelectorAll('.card .title') on a DOM element, which elements can the '.card' part of that selector match, and what does the :scope pseudo-class change?

level: middleimportance: should knowfreq 44%

basics

~20 s

The selector is matched against the whole tree, and only the results are filtered to descendants of the element you called it on, so '.card' may match an ancestor outside the widget. The :scope pseudo-class anchors the selector to that element.

open as a page

In the browser DOM, when do node.parentNode and node.parentElement return different things, and which should a loop that climbs toward the root use?

level: middleimportance: should knowfreq 35%

basics

~20 s

They agree whenever the parent is an element. They differ when the parent is not one: for the <html> element, parentNode is the Document but parentElement is null, and inside a DocumentFragment parentNode is the fragment while parentElement is null. Climbing loops should use parentElement.

open as a page

An element inside a shadow root dispatches new CustomEvent('changed', { bubbles: true }), but a listener on document never fires. Which flag is missing, and what else changes once you set it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The composed flag is missing. It defaults to false, so a bubbling event stops at the shadow root. Adding composed: true lets it continue into the host's tree, where listeners see event.target retargeted to the host element.

open as a page

A dropdown closes itself using a click listener on document. Inside an unrelated modal, a click handler calls event.stopPropagation(), and now the dropdown never closes. Explain the mechanism, and how you would fix it without breaking the modal.

level: seniorimportance: should knowfreq 50%

basics

~20 s

The modal's call ends the dispatch before it reaches document, so the dismiss listener never runs. Fix it by listening on document in the capture phase, which runs before any descendant handler, or by removing the stopPropagation and coordinating some other way.

open as a page

What does { passive: true } promise the browser when you register a DOM listener with addEventListener, and what happens if that listener calls preventDefault()?

level: seniorimportance: should knowfreq 48%

basics

~20 s

It promises the listener will never cancel the event's default action, so the browser can start scrolling immediately instead of waiting to see what the listener does. preventDefault() inside a passive listener is a no-op; Chrome logs a console warning when you try.

open as a page

You move a live subtree into a different container by appending it there. The nodes are not re-created, so what state is still lost, and how do you protect what matters?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Anything the browser derives from the node being in a rendered document is discarded: focus, text selection, a scrollable element's offset, running CSS transitions and animations, and an iframe's loaded document, which reloads. Save that state before the move and restore it after.

open as a page

A custom slider tracks a drag with pointerdown on the thumb plus pointermove and pointerup on the same element. The drag stops updating as soon as the pointer moves off the thumb. What does Element.setPointerCapture() change, and what else must the handler deal with?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Without capture, pointermove is dispatched by hit-testing, so it goes to whatever element is under the pointer once it leaves the thumb. setPointerCapture(pointerId) retargets every later event for that pointer to the capturing element until pointerup or pointercancel, which must also reset the drag state.

open as a page

A dropdown closes itself with a document-level check `if (!panel.contains(clickedNode)) close()`, and it wrongly closes when the user clicks a control inside the panel that re-renders its contents. Why can panel.contains(clickedNode) be false for a node that was inside the panel, and how do you make the containment check reliable?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Node.contains() reports the tree as it is at call time. If the clicked node was removed from the DOM by an earlier re-render, it is now detached, so contains() returns false. Decide containment while the node is still connected, or check isConnected first.

open as a page

showing 1–30 of 36