2026-09-27 ยท 10 min read
JavaScript Concepts I Wish I Understood Earlier: Event Bubbling & Event Delegation
"With AI writing the code, maybe it's time we finally understand what the code is doing." ๐
The rise of AI has made writing code easier than ever. It has also made one thing increasingly important for me: actually understanding JavaScript.
After a year away from frontend development, I found myself coming back to JavaScript feeling more than a little disoriented, especially with the AI boom, and the temptation to just let a model generate everything for me.
AI Fears vs AI Cheers.
Instead, I saw a golden opportunity to properly understand JavaScript, and decided to go back to the fundamentals. Not "what does this syntax do?" fundamentals. More like:
"Why does JavaScript work this way, and how could I have written this better?"
That led me to revisit some of my old projects. And that's where I found some code from 2024 that made me think:
Past me knew JavaScript.
Past me now has some explaining to do. ๐
This is the beginning of a series about JavaScript concepts I'm learning โ or relearning โ and the things I wish I had understood earlier.
1. Event Bubbling and Event Delegation
Let's start with something simple. Imagine you're building an FAQ section with accordion/drop down buttons for each question.
FAQ Accordion: Front end mentor challenge.
You have five questions. Clicking the button on a question should reveal its answer, while closing any other open answer.
Something like this:
<div class="acc">
<div class="acc-item" data-id="1">
<h3 class="acc-title">Question 1</h3>
<button class="acc-icon-container">+</button>
<p class="acc-content hide">Answer 1</p>
</div>
<div class="acc-item" data-id="2">
<h3 class="acc-title">Question 2</h3>
<button class="acc-icon-container">+</button>
<p class="acc-content hide">Answer 2</p>
</div>
<div class="acc-item" data-id="3">
<h3 class="acc-title">Question 3</h3>
<button class="acc-icon-container">+</button>
<p class="acc-content hide">Answer 3</p>
</div>
</div>
Pretty straightforward. But the interesting part isn't the accordion. It's how we listen for the clicks.
How I used to do it
Back in 2024, I approached the problem by grabbing all the buttons, all the content, and all the headers separately:
let accordionBtns = document.querySelectorAll(".acc-icon-container");
let accordionContent = document.querySelectorAll(".acc-content");
let accordionHeader = document.querySelectorAll(".acc-title");
Then I added an event listener to every button:
for (let x = 0; x < accordionBtns.length; x++) {
accordionBtns[x].addEventListener("click", function () {
hideOtherAccordionContentExceptThis(x);
toggleAccordion(accordionContent[x], x);
});
}
At the time, this made perfect sense to me. Button #1 corresponds to content #1. Button #2 corresponds to content #2. Button #3 corresponds to content #3. So I could use the index to keep everything connected:
accordionBtns[2]
โ
accordionContent[2]
โ
accordionHeader[2]
It worked. But there's a problem: the index was doing a lot of work.
My code depended on the assumption that all these NodeLists would always have the same structure and order. That's why I was passing x everywhere โ into toggleAccordion(accordionContent[x], x) and hideOtherAccordionContentExceptThis(x). The x was essentially carrying information around because my elements didn't have a direct relationship in the JavaScript itself.
Me in 2024: "Don't worry bro, index 2 knows what index 2 means."
Me in 2026: "That's... not ideal." ๐
I also had this:
let currentClassList = Array.from(item.classList);
if (currentClassList.includes("hide")) {
At the time, I thought "classList isn't an array, so let me convert it into one." Technically, that works. But I was solving a problem that didn't exist โ classList already gives us methods designed for exactly this, like item.classList.contains("hide") or item.classList.toggle("hide").
That was another small lesson: before converting something into another data structure, check whether the object already has the functionality you need.
Enter Event Delegation
When I came back to this project, I wanted to refactor it. Instead of attaching an event listener to every accordion button, I could attach one listener to the parent container:
const accordionList = document.querySelector(".acc");
accordionList.addEventListener("click", (event) => {
// ...
});
One listener for the entire accordion. But how does the parent know which button was clicked? That's where event bubbling comes in.
When you click an element, the event doesn't necessarily stay there โ it bubbles up through its ancestors:
.acc
โ
โโโ .acc-item
โ
โโโ button
You click the button. The click happens on the button. The event then bubbles upward:
button
โ
.acc-item
โ
.acc
So even though our event listener is attached to accordionList, we can still determine what was clicked. That's the foundation of event delegation.
event.target and .closest()
Inside our event listener:
accordionList.addEventListener("click", (event) => {
console.log(event.target);
});
event.target tells us the element where the event originated. If I click <button class="acc-icon-container">+</button>, event.target points to that button. Now we can use .closest() to find our way back up:
const toggleBtn = event.target.closest(".acc-icon-container");
if (!toggleBtn) return; // guard clause: click wasn't on a toggle button
Then we move upward again:
const accordionItem = toggleBtn.closest(".acc-item");
Now we have the specific accordion item containing the button that was clicked, and from there we can move back down into it:
const accordionHeader = accordionItem.querySelector(".acc-title");
const currentParagraph = accordionItem.querySelector(".acc-content");
The mental model becomes:
User clicks button
โ
event.target
โ
closest(".acc-icon-container")
โ
closest(".acc-item")
โ
querySelector(".acc-title")
querySelector(".acc-content")
Or, in plain English: "I clicked this button. Which accordion item does it belong to? Now give me the elements inside that accordion item." That's a much more natural relationship than "give me item number 3 because button number 3 was clicked."
The Refactored Version
Here's where the pieces come together:
const accordionList = document.querySelector(".acc");
accordionList.addEventListener("click", (event) => {
const toggleBtn = event.target.closest(".acc-icon-container");
if (!toggleBtn) return;
const accordionItem = toggleBtn.closest(".acc-item");
const accordionHeader = accordionItem.querySelector(".acc-title");
const currentParagraph = accordionItem.querySelector(".acc-content");
// Reset all other accordion items
accordionList.querySelectorAll(".acc-item").forEach((item) => {
if (item !== accordionItem) {
item.querySelector(".acc-content").classList.add("hide");
item.querySelector(".acc-title").classList.remove("bold");
item.querySelector(".acc-icon-container").classList.remove("rotate");
}
});
// Toggle current accordion
currentParagraph.classList.toggle("hide");
accordionHeader.classList.toggle("bold");
toggleBtn.classList.toggle("rotate");
});
The icon rotation can be handled with CSS:
.acc-icon-container {
transition: transform 0.3s ease;
}
.acc-icon-container.rotate {
transform: rotate(180deg);
}
Now we have one event listener on .acc, asking "which button? which accordion item? which content/header/icon?" โ instead of five separate listeners each hoping their index lines up with everyone else's.
My old code was based on indexes: accordionBtns[x], accordionContent[x], accordionHeader[x]. My new code is based on relationships in the DOM: toggleBtn โ accordionItem โ accordionHeader / currentParagraph. The second approach is easier to reason about because the DOM itself describes the relationship โ each .acc-item already contains everything it needs, so instead of maintaining separate arrays and hoping their indexes stay synchronized, I can start from the element I interacted with and navigate to what's related to it.
There's also a small toggle() lesson buried in here. Originally I was manually checking whether a class existed:
if (currentClassList.includes("hide")) {
item.classList.remove("hide");
} else {
item.classList.add("hide");
}
But JavaScript already gives us item.classList.toggle("hide") โ and the same idea covers accordionHeader.classList.toggle("bold") and toggleBtn.classList.toggle("rotate"). Instead of saying "if the class exists, remove it, otherwise add it," I'm saying "toggle this state." Sometimes the best refactor isn't shorter code โ it's code that says what you actually mean.
Why This Matters Beyond a Five-Button Accordion
You might be thinking: "Okay, but is this really necessary for five buttons?" Five event listeners aren't going to destroy your browser. The real value is the pattern, it becomes essential once you're dealing with larger or dynamically generated UIs.
With individual listeners, every new button needs its own listener attached by hand. With delegation, one listener on the parent handles clicks from any child: including children that don't exist yet. If you add another accordion item later, the parent listener handles clicks on it automatically, with nothing extra to wire up.
What I Actually Learned
- Events bubble. A click on a child element propagates upward through its ancestors.
- Event delegation lets a parent handle events for its children, instead of attaching a listener to every single one.
event.targettells us where the event originated..closest()lets us navigate upward to the nearest matching ancestor..querySelector()lets us navigate back down from there.- DOM relationships beat parallel arrays.
buttons[2] โ content[2]is fragile; "this button โ this accordion item โ this content" isn't. - Check for built-in methods before reaching for a conversion โ
classListalready does most of what you need.
Looking back at my 2024 code, I wouldn't call it bad code โ it solved the problem, and that's worth acknowledging. You don't learn JavaScript by writing the perfect solution the first time. You write something that works, learn another concept, then look back and think "I could have done this better." That's not failure. That's progress.
With AI making it increasingly easy to generate code, I think there's even more value in being able to look at what you wrote and think: "Yeah, I know why this works." That's the level of JavaScript knowledge I'm trying to build, and why I'm going back through my old projects, not to rewrite everything for its own sake, but to understand why I wrote things the way I did, and what I'd do differently now.
Next up
There's plenty more to revisit: closures, this, higher-order functions, destructuring, spread & rest operators, promises & async/await, the event loop, DOM manipulation, fetch & APIs, modules, prototypes, optional chaining & nullish coalescing โ and eventually, how all of this connects to React.
Because apparently, "I know JavaScript" and "I understand JavaScript" are two completely different sentences. ๐ See you on the next one.