DOM events and delegation
Understand capture, target, bubbling, default actions, and scalable event delegation.

WHAT YOU WILL BE ABLE TO DO
Learning outcomes
- Explain dom events and delegation in plain language.
- Connect the behavior to the underlying browser or framework model.
- Implement the core pattern and reason through edge cases.
- Answer common follow-ups without relying on memorized phrases.
01Explain it simply
An event travels through the DOM: capture from the document toward the target, runs at the target, and usually bubbles back toward the document. Delegation uses that bubbling path to handle many descendants with one listener.
One-line definition: Understand capture, target, bubbling, default actions, and scalable event delegation.
02Mental model
Separate propagation from default behavior. stopPropagation controls where the event travels; preventDefault cancels the browser action when the event is cancelable. They solve different problems.
03Step by step
- Register a listener on the nearest stable ancestor.
- Read event.target to find where the event began.
- Use closest to match an intended interactive descendant.
- Confirm the matched node belongs to the current container.
- Handle the action using data attributes or DOM state.
04Working example
const list = document.querySelector('[data-list]');
list.addEventListener('click', (event) => { const button = event.target.closest('[data-remove]'); if (!button || !list.contains(button)) return; button.closest('li')?.remove();});New list items work automatically because the listener belongs to the stable list container, not to each button. closest also handles clicks on an icon nested inside the button.
05Where it is used
- Dynamic lists and tables
- Menus and command surfaces
- Reducing listener count
- Analytics attached at a stable page boundary
06Common mistakes
- Using event.target when currentTarget is intended
- Calling stopPropagation globally and breaking other features
- Delegating non-bubbling events without checking behavior
- Matching a node outside the intended container
07Interview answer
Trace capture, target, and bubble explicitly, then show why delegation works for dynamically inserted descendants.
What is the difference between target and currentTarget?
target is where the event originated; currentTarget is the element whose listener is currently running.
DDConcept deep dives
Deep dive 1
Propagation and default action are independent
An event follows a propagation path through capture, target, and usually bubble phases. Separately, the browser may perform a default action such as following a link or submitting a form. stopPropagation changes which listeners receive the event; preventDefault requests cancellation of a cancelable default. Using one when you mean the other creates surprising behavior.
- currentTarget is the node whose listener is running; target identifies the origin after retargeting rules.
- stopImmediatePropagation also blocks later listeners on the same node.
- Passive listeners promise not to cancel, enabling scrolling optimizations.
Deep dive 2
Delegation makes a stable ancestor the owner
A single listener on a container can handle descendant interactions because many events bubble. The handler finds the intended control with closest and then verifies the match belongs to the current component boundary. New children work automatically because no per-item registration is required.
- Nested icons make direct target matching fragile; closest follows ancestors.
- Nested components can produce false matches unless ownership is verified.
- Focus and blur do not bubble in the same form as focusin and focusout.
list.addEventListener('click', (event) => { const button = event.target.closest('button[data-remove]'); if (!button || button.closest('[data-list]') !== list) return; removeItem(button.dataset.remove);});The second boundary check prevents this list from consuming a remove button owned by a nested list.
Deep dive 3
Listener lifetime is part of component lifetime
A registered listener creates a reachability path from its event target to the callback and captured state. Remove it using the same type, callback identity, and capture option, or register with an AbortSignal and abort at teardown. Global targets deserve particular care because they commonly outlive routed UI.
- An inline function passed separately to add and remove is not the same callback.
- The once option automatically removes a listener after its first invocation.
- Delegation can reduce registration work but should not cross ownership boundaries indiscriminately.
QAInterview questions and model answers
Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.
Intermediate · Conceptual · 1 min · Question 1What are the phases of DOM event propagation?Open model answer
Model answer
The event travels from the root toward the target during capture, reaches the target, and—if it bubbles—travels back through ancestors. Listeners can register for capture; most application delegation uses bubbling.
Open question page →Intermediate · Coding · 1 min · Question 2How do target and currentTarget differ?Open model answer
Model answer
target is the node where the event originated, subject to retargeting across boundaries. currentTarget is the node whose listener is currently executing. In a delegated handler, currentTarget is the container while target may be a nested icon.
list.addEventListener('click', (e) => { e.currentTarget; // always `list` (where the listener lives) e.target; // the actual clicked node, e.g. an <svg> inside a button});Intermediate · Coding · 1 min · Question 3What is event delegation?Open model answer
Model answer
It attaches one listener to a stable ancestor and identifies matching descendants from the event path, commonly with closest. It handles dynamically inserted children and reduces listener setup, but requires attention to non-bubbling events and nested matching boundaries.
document.querySelector('#todo-list').addEventListener('click', (e) => { const btn = e.target.closest('button[data-action="delete"]'); if (!btn) return; deleteTodo(btn.closest('li').dataset.id); // works for rows added later});Intermediate · Conceptual · 1 min · Question 4How do preventDefault and stopPropagation differ?Open model answer
Model answer
preventDefault cancels a cancelable browser action such as link navigation or form submission. stopPropagation stops further travel through the propagation path. Neither implies the other.
Open question page →Intermediate · Conceptual · 1 min · Question 5What does stopImmediatePropagation do?Open model answer
Model answer
It prevents later listeners on the same node from running and also stops propagation to other nodes. It creates strong coupling and should be rare in application code because it can silently break unrelated behavior.
Open question page →Advanced · Conceptual · 1 min · Question 6How do Shadow DOM boundaries affect events?Open model answer
Model answer
Only composed events cross a shadow boundary, and event.target may be retargeted to preserve encapsulation. event.composedPath() exposes the propagation path appropriate to the event and is useful for boundary-aware delegation.
Open question page →Advanced · Conceptual · 1 min · Question 7What is event.composedPath useful for?Open model answer
Model answer
It returns the event's propagation path across relevant shadow boundaries, helping code understand retargeted events without assuming ordinary parent traversal.
Open question page →Advanced · Conceptual · 1 min · Question 8Do mouseenter and mouseleave bubble?Open model answer
Model answer
They do not bubble like mouseover and mouseout. Delegation may use bubbling alternatives while checking relatedTarget, or pointer events suited to the interaction.
Open question page →Beginner · Conceptual · 1 min · Question 9What does the once listener option do?Open model answer
Model answer
The browser removes the listener automatically after its first invocation. This makes single-use ownership explicit and avoids manual cleanup for that path.
Open question page →Advanced · Conceptual · 1 min · Question 10Why use an AbortSignal with addEventListener?Open model answer
Model answer
Passing a signal lets one abort operation remove related listeners, simplifying teardown when a component or request owns several event registrations.
Open question page →SCScenario questions
Scenario 1
A delegated remove handler activates when clicking a matching element inside a nested list owned by another component.
- Find the closest matching control from the event target.
- Verify the matched control belongs to the intended container.
- Use component boundaries or data ownership markers.
- Test nested and dynamically inserted structures.
Reveal worked answer
closest may return a valid-looking descendant from a nested component. After matching, I would verify the control's nearest owning list is the currentTarget, or otherwise encode ownership explicitly. That prevents a parent delegate from handling actions owned by a nested widget.