HuntExams Academy logo
JavaScriptIntermediate#dom#events

What is event bubbling, capturing and delegation?

Answer

Events travel down (capture) to the target and then bubble up. Delegation exploits bubbling by putting one listener on a parent and inspecting event.target, which is efficient for dynamic lists.

Example

list.addEventListener('click', e => {
  const li = e.target.closest('li');
  if (li) console.log('clicked', li.dataset.id);
});

Detailed explanation

Event capturing, bubbling, and delegation describe how DOM events reach listeners and how you can use that behaviour to organise event handling. When someone clicks a nested element, the browser dispatches the event along an event path. For a typical element in a document, capturing proceeds from window through document and ancestor elements towards the target. The target phase runs listeners on the target itself; if the event bubbles, the bubbling phase then proceeds back through its ancestors. This explains why clicking a button inside a container can also trigger the container’s click listener.

By default, addEventListener('click', handler) registers a listener for the target or bubbling phase. Passing { capture: true } registers a capturing listener instead. On the target itself, capturing listeners run before non-capturing listeners. event.target identifies the element where the event originated in ordinary DOM usage, whereas event.currentTarget identifies the element whose listener is currently executing. These properties are especially important when a button contains an icon: clicking the icon makes the icon the target, but a listener attached to the button still sees the button as currentTarget.

Event delegation usually exploits bubbling by attaching one listener to a stable ancestor instead of attaching listeners to every child. In the list example, e.target.closest('li') finds the clicked list item even when the actual click lands on a nested span. For robust code, first check that e.target is an Element, then check that the matching item belongs to the intended list. For direct children, li.parentElement === list avoids accidentally accepting items from a nested list. Because the parent listener remains in place, newly inserted items work without additional listener registration, reducing listener management and simplifying cleanup.

Interviewers expect you to explain the limitations as well as the benefit. Not every event bubbles: focus and blur do not, while focusin and focusout do. stopPropagation() prevents further propagation along the path; stopImmediatePropagation() also stops remaining listeners on the current element. preventDefault() is different: it cancels a cancelable default action, such as link navigation, without stopping propagation. Delegation also depends on the event reaching the ancestor, so a descendant that stops propagation can break a bubbling-based delegated handler.

Key points to remember

  • Event dispatch has capturing, target, and, for bubbling events, bubbling phases.
  • Use { capture: true } to register a capturing listener; the default is non-capturing.
  • event.target is the originating element in ordinary DOM usage; event.currentTarget is the listener’s element.
  • Delegation handles child interactions through an ancestor listener, including interactions on dynamically added children.
  • Use closest() to handle nested click targets, and verify that the match belongs to the intended container.

Common mistakes candidates make

  • Assuming every event bubbles or that preventDefault() stops propagation.
  • Using event.target directly as the list item, ignoring clicks on icons or spans inside it.
  • Accepting any closest('li') match without checking container ownership, especially with nested lists.

Likely follow-up questions

How would you make the list example safer?

Guard with if (!(e.target instanceof Element)) return, then find const li = e.target.closest('li'). If only direct list children should match, return when !li || li.parentElement !== list before reading li.dataset.id.

Can you delegate events that do not bubble?

Sometimes you can listen on an ancestor during capture, as with focus and blur. Alternatively, use a bubbling counterpart such as focusin or focusout; capturing is not a universal substitute for every non-bubbling event.

What happens if a child listener calls stopPropagation()?

A bubbling listener on the parent will not receive that event. Ancestor capturing listeners that already ran are unaffected, and other listeners on the child still run unless stopImmediatePropagation() is called.

Interview tip: Explain the event path first, demonstrate delegation with a nested click target, and finish by distinguishing target, currentTarget, and propagation control.

Related Interview Questions

1
JavaScriptIntermediate#dom#events

Difference between stopPropagation and preventDefault?

Open
2
JavaScriptAdvanced#performance#functions

What is debouncing and throttling?

Open
3
JavaScriptBeginner#arrays

Explain map, filter and reduce.

Open