Why Does Event Bubbling Happen by Default? (And Why It Makes Sense)
When studying event bubbling, you might wonder: Why does it happen by default? Why do we need to explicitly call stopPropagation()
to prevent it?.
If you are not clear on what event bubbling is, check out my previous aritcle.
The short answer is: it’s intentional and very important.
Let’s break down the reasons why event bubbling is enabled by default in the browser.
Consistent Event Handling Behavior in Parent Elements
It is the core reason of event bubbling happen by default.
Take a look at the following example:
<body>
<div class="card">
<h3>Card Heading</h3>
<div class="card-img">
<img src="water-ripple.jpg" alt="" />
</div>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Officiis
doloremque harum repellat nesciunt! Mollitia repellat accusamus,
distinctio in dolor aperiam!
</p>
<button>Button</button>
</div>
<script>
const card = document.querySelector(".card");
card.addEventListener("click", (e) => {
alert("Card clicked =========");
});
</script>
</body>
Without Bubbling?
In this example, we have a card component with children: a heading, paragraph, button, and an image inside a div. What do we usually expect? That clicking anywhere on the card — whether on the text, image, or button — will trigger the card’s click event, right?
Yes it is because of the bubbling. Because clicking of any of the children propagate into parent.
If bubbling didn’t happen by default, then clicking on the image, button, or heading wouldn’t trigger the card’s click event — and that would create a bad UX in many situations.
Let's simulate it.
const card = document.querySelector(".card");
const cardHeading = document.querySelector(".card h3");
const cardParagraph = document.querySelector(".card p");
const cardButton = document.querySelector(".card button");
const cardDiv = document.querySelector(".card div");
card.addEventListener("click", (e) => {
alert("Card clicked =========");
});
/* these is for simulating for if even bubbling for click no happen by default*/
cardHeading.addEventListener("click", (e) => {
e.stopPropagation();
});
cardDiv.addEventListener("click", (e) => {
e.stopPropagation();
});
cardParagraph.addEventListener("click", (e) => {
e.stopPropagation();
});
cardButton.addEventListener("click", (e) => {
e.stopPropagation();
});
Here I simulated all children of card click event bubbling prevented. If it happen by default then it feels like:
So then to trigger children of card we explicitly have to target all children and one by one enable bubbling. And not in one place rather most of the place where we need that feature.
Imagine a card with 20 child elements. Without bubbling, you’d have to attach individual click handlers to each of them just to simulate a single action — sounds like a nightmare, right?
So, to allow a feature where clicking anywhere on the card triggers an action, you’d have to manually enable event propagation for each child — one by one.
Hypothetically assuming event have a method name event.enablePropagation()
(that enable propagation or bubbling), then code will be look like following.
const card = document.querySelector(".card");
const cardHeading = document.querySelector(".card h3");
const cardParagraph = document.querySelector(".card p");
const cardButton = document.querySelector(".card button");
const cardDiv = document.querySelector(".card div");
card.addEventListener("click", (e) => {
alert("Card clicked =========");
});
/* these is for simulating for if even bubbling for click no happen by default */
cardHeading.addEventListener("click", (e) => {
event.enablePropagation(); // it doesn't exist in javascript, assuming hypothetically
});
cardDiv.addEventListener("click", (e) => {
event.enablePropagation();
});
cardParagraph.addEventListener("click", (e) => {
event.enablePropagation();
});
cardButton.addEventListener("click", (e) => {
event.enablePropagation();
});
Or second way is we have to copy and past same event in every child if we want to find second possible way.
const card = document.querySelector(".card");
const cardHeading = document.querySelector(".card h3");
const cardParagraph = document.querySelector(".card p");
const cardButton = document.querySelector(".card button");
const cardDiv = document.querySelector(".card div");
card.addEventListener("click", (e) => {
alert("Card clicked =========");
/*
other code
======
*/
});
/* these is for simulating for if even bubbling for click no happen by default */
cardHeading.addEventListener("click", (e) => {
alert("Card clicked =========");
/*
other code
======
*/
});
cardDiv.addEventListener("click", (e) => {
alert("Card clicked =========");
/*
other code
======
*/
});
cardParagraph.addEventListener("click", (e) => {
alert("Card clicked =========");
/*
other code
======
*/
});
cardButton.addEventListener("click", (e) => {
alert("Card clicked =========");
/*
other code
======
*/
});
And in world of web app we mostly need that feature frequently. This would be a nightmare to manage — especially in large UIs — and would break intuitive behavior where clicking anywhere on a card should trigger the same action.
To solve that problem by default event bubbling enabled.
Conclusion:
Event bubbling not only solve this issue it powers many useful patterns in front-end development. I will writing more on that in future.
Enabling bubbling by default helps to optimize event handling. Because naturally our brain assume that any child inside an element also will inherit or affect the parent's behavior just like human. And then if you need something different there are way to modify.
But just like in real life, children don’t always behave exactly like their parents — and when needed, we can stop or customize propagation using stopPropagation()
.
Thanks for reading the article if it was helpful then please share it in X, LinkedIn.
Here's my LinkedIn profile Here's my GitHub profile