Event delegation and event propagation in JavaScript

1. Event propagation

Event propagation is the way events travel through the DOM tree. There are two directions of propagation:

  • Capturing phase (downwards) – the event travels from the document element down to the element that triggered it.
  • Bubbling phase (upwards) – the event travels from the element that triggered it up through the DOM tree until it reaches document.

Example of event propagation

Let’s assume we have the following HTML:

<div id="parent">
    <button id="child">Click me</button>
</div>

And we add two event listeners:

document.getElementById("parent").addEventListener("click", () => {
    console.log("PARENT clicked");
});

document.getElementById("child").addEventListener("click", () => {
    console.log("CHILD clicked");
});

If you click on #child, you will see in the console:

CHILD clicked!
PARENT clicked!

This happens because the event bubbles (it travels from the child up to the parent).

Stopping event propagation

If we don’t want an event to bubble up, we can use event.stopPropagation()

document.getElementById("child").addEventListener("click", event => {
    event.stopPropagation();       // Stopas propagation
    console.log("CHILD clicked");
});

Now, when you click #child, the console will show only: CHILD clicked, and #parent will not receive the event.

Forcing event capturing

By default, event listeners work in the bubbling phase, which means the event for #child will fire first, followed by the event for #parent.
However, you can reverse this order by assigning the listener to the capturing phase, by passing true as the third argument to addEventListener.

document.getElementById("parent").addEventListener("click", () => {
    console.log("PARENT clicked (capturing)");
}, true);

Now #parent will fire before #child, because it handles the event during the capturing phase.

2. Event delegation

Event delegation is a technique of assigning an event listener to a parent element instead of attaching one to each child individually. It’s useful when elements are added to the DOM dynamically (e.g., via AJAX).

document.getElementById("parent").addEventListener("click", event => {
  if (event.target.classList.contains("child")) {
    console.log("Child clicked: ", event.target);
  }
});

If we later add a new element:

const newChild = document.createElement("div");
newChild.classList.add("child");
newChild.textContent = "New element";
document.getElementById("parent").appendChild(newChild);

We will still be able to detect clicks on the new element, even though we didn’t attach a separate addEventListener to it.