Big Bright Pixels

A look at the browser's event life cycle

October 06, 2019

This is one of my favorite concepts to ask candidates about during an interview. The browser’s event life cycle is something that a front-end developer can develop against for some time without fully understanding. I find it to be a good gauge of a person’s experience and overall knowledge of the web platform, especially since responding to events is so central to the applications we build.

Event-driven architecture

Very briefly, an event-driven architecture is a software architectural pattern that enables loosely coupled components and services. An event-driven system consists of event emitters and consumers. The event emitter is responsible for detecting and transmitting an event; it is unaware of any consumers that may or may not exist. Conversely, the event consumer is responsible for reacting to a given event.

Even with this very brief definition, it should be apparent that the browser’s Document Object Model (DOM) is an event-driven system. The browser is responsible for detecting the different classes of events, e.g. ‘click’, ‘load’, etc. It is the event emitter.

When we attach an event listener to a DOM element using addEventListener, as in the following example, it becomes the event consumer.

const onClick = event => console.log('event', event);
const someButton = document.getElementById('some-button');
someButton.addEventListener('click', onClick);

Event life cycle

Now that we have a high-level understanding of the event-driven architectural pattern and how the browser and DOM elements fit into that paradigm, let’s take a deeper look into the event life cycle. For our analysis, we’ll reference the following basic HTML document:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Event life cycle</title>
  </head>
  <body>
    <button id="button">Activate!</button>
  </body>
</html>

When the above HTML document is parsed, the following Abstract Syntax Tree (AST) is generated by the browser, which we will reference through what follows.

ast generated from the example html

The event life cycle consists of 3 phases:

  1. Capture Phase
  2. Target Phase
  3. Bubble Phase

Capture Phase

In the Capture Phase, the event is dispatched from the root ancestor to each ancestor element in the tree until it reaches the target, at which point the Target Phase begins.

Using our reference HTML document, the event will go from the document to the body elements as part of the capture Phase.

Target Phase

In the Target Phase, the event is dispatched to the target of the event.

If the button is clicked, it is the event.target. During the Target Phase, if the button has a registered event listener, it will be triggered.

Bubble Phase

In the Bubble Phase, the event “bubbles” or goes back down the tree through each ancestor in the tree until it reaches the root ancestor.

Using our reference HTML document, the event will go from the body to the document, where the event life cycle ends.

Listening for events

While an individual event goes through all 3 phases of the event life cycle, we control whether an element (the event consumer) listens to the Capture or Bubble Phase.

You will recall that addEventListener takes 3 parameters. The first parameter is the event type as a string. The second is the function called when the specified event occurs. The third parameter is flexible.

If you pass a false or the leave the third parameter undefined, it means that you want this event listener to trigger in the Target and Bubble phases.

If you pass true, though, it means you want the listener to trigger during the Capture and Target phases.

You can also pass an options object as the third parameter of addEventListener. If you pass { capture: true }, it means you want the listener to trigger during the Capture and Target phases. This is the same as passing true as the third parameter.

Listening to the Target and Bubble phases

Let’s assume we have the following event listeners attached to the document, body, and button, and a user clicks on the button. Notice that we’re leaving addEventListener’s third parameterundefined.

const phases = {
  1: 'capture',
  2: 'target',
  3: 'bubble',
};
const onClick = (event) => console.log(`
  currentTarget: ${event.currentTarget}
  target: ${event.target}
  phase: ${phases[event.eventPhase]}
`);
const button = document.getElementById('button');
document.addEventListener('click', onClick);
document.body.addEventListener('click', onClick);
button.addEventListener('click', onClick);

The resultant console output will be:


currentTarget: [object HTMLButtonElement]
target: [object HTMLButtonElement]
phase: target


currentTarget: [object HTMLBodyElement]
target: [object HTMLButtonElement]
phase: bubble


currentTarget: [object HTMLDocument]
target: [object HTMLButtonElement]
phase: bubble

Listening to the Capture and Target phases

Let’s assume we have the following event listeners attached to the document, body, and button, and a user clicks on the button. Here we’re going to pass the options object, but we could have achieved the same thing by passing true as addEventListener’s third parameter.

document.addEventListener('click', onClick, { capture: true });
document.body.addEventListener('click', onClick, { capture: true });
button.addEventListener('click', onClick, { capture: true });

The resultant console output will be:


currentTarget: [object HTMLDocument]
target: [object HTMLButtonElement]
phase: capture


currentTarget: [object HTMLBodyElement]
target: [object HTMLButtonElement]
phase: capture


currentTarget: [object HTMLButtonElement]
target: [object HTMLButtonElement]
phase: target

In the above example, you’ll notice that even though we specified { capture: true } on the button’s addEventListener, it was ignored because it was the target of the event.

Listening to a mix of phases

You can mix and match to which phases elements listen. For example, let’s say you want the document to listen to the Bubble Phase and the body to listen during the Capture Phase:

document.addEventListener('click', onClick);
document.body.addEventListener('click', onClick, { capture: true });
button.addEventListener('click', onClick);

The resultant console output will be what you’d expect:

currentTarget: [object HTMLBodyElement]
target: [object HTMLButtonElement]
phase: capture

currentTarget: [object HTMLButtonElement]
target: [object HTMLButtonElement]
phase: target


currentTarget: [object HTMLDocument]
target: [object HTMLButtonElement]
phase: bubble

The stopPropagation method

The Event’s stopPropagation method prevents further propagation of an event in the Capture, Target, or Bubble phases, depending on which phase the element is listening.

Given our previous example where the document is listening to the Bubble Phase and the body is listening to the Capture Phase, let’s adjust it slightly so that we stop propagating the event in the body’s addEventListener in the Capture Phase.

document.addEventListener('click', onClick);
document.body.addEventListener('click', (event) => {
  event.stopPropagation();
  onClick(event);
}, { capture: true });
button.addEventListener('click', onClick);

The resultant console output will be:

currentTarget: [object HTMLBodyElement]
target: [object HTMLButtonElement]
phase: capture

Having stopped propagation in the Capture Phase, we see that the event never reaches the button (or the Target Phase); nor does it get dispatched to the document in the Bubble Phase.