๐ What is Event Delegation?
Event delegation is a technique where instead of adding event listeners to multiple child elements,
you add one event listener to the parent, and handle events using event bubbling.
โ Why Use Event Delegation?
โ Fewer event listeners โ better performance
โ Works for elements added later (dynamic content)
โ Cleaner and maintainable code
โ Useful for lists, tables, menus, forms, etc.
๐ง How It Works
A child element is clicked
The event bubbles up to the parent
The parent checks which child triggered the event using
event.target
โ Example Without Event Delegation (Not efficient)
item1.addEventListener('click', handleClick);
item2.addEventListener('click', handleClick);
item3.addEventListener('click', handleClick);โ Hard to maintain
โ Breaks when new items are added
โ Event Delegation โ Full Working HTML Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Event Delegation Example</title>
</head>
<body>
<h2>Click any item</h2>
<ul id="lists">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<button id="add">Add New Item</button>
<script>
const lists = document.getElementById('lists');
const addBtn = document.getElementById('add');
// โ
Event delegation on parent <ul>
lists.addEventListener('click', (e) => {
if (e.target.tagName === 'LI') {
alert('You clicked: ' + e.target.textContent);
}
});
// โ
Dynamically adding new items (still works!)
addBtn.addEventListener('click', () => {
const newItem = document.createElement('li');
newItem.textContent = 'New Item';
list.appendChild(newItem);
});
</script>
</body>
</html>Why This Works
The click event on
<li>bubbles to<ul><ul>listener checks the clicked element usinge.targetNo need to add listeners to every
<li>Even newly added items are detected โ
<div class="infographic-delegconcept">
<style>
.infographic-delegconcept {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--line: #ddd6c9;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--ok: #1a7f4e;
--ok-soft: #e3f5ec;
--bad: #b42318;
--bad-soft: #fbe9e7;
--display-font: 'Fraunces', Georgia, serif;
--body-font: 'Inter', -apple-system, sans-serif;
--mono-font: 'IBM Plex Mono', 'Courier New', monospace;
margin: 0 auto;
box-sizing: border-box;
font-family: var(--body-font);
color: var(--ink);
background: var(--paper);
}
.infographic-delegconcept[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--line: #2c313d;
--accent: #5b8def;
--accent-soft:#202942;
--ok: #5cd6a0;
--ok-soft: #16342a;
--bad: #e57373;
--bad-soft: #3a2222;
}
.infographic-delegconcept * { box-sizing: border-box; }
.infographic-delegconcept .ig-inner { padding: 1.75rem clamp(1.25rem, 5vw, 3rem) 2rem; }
.infographic-delegconcept .ig-title {
font-family: var(--display-font); font-size: clamp(1.4rem, 4.5vw, 1.9rem);
font-weight: 600; text-align: center; margin: 0 0 1.5rem;
}
.infographic-delegconcept .toggle-row {
display: flex; align-items: center; justify-content: center; gap: 0.75rem; margin-bottom: 2rem;
}
.infographic-delegconcept .toggle-label {
font-family: var(--mono-font); font-size: 0.82rem; font-weight: 700; color: var(--ink-soft); transition: color 0.2s; cursor: pointer;
}
.infographic-delegconcept .toggle-label.active.without { color: var(--bad); }
.infographic-delegconcept .toggle-label.active.with { color: var(--ok); }
.infographic-delegconcept .switch {
position: relative; width: 3.4rem; height: 1.8rem; border-radius: 999px; cursor: pointer;
background: var(--bad); transition: background 0.3s;
}
.infographic-delegconcept .switch.is-on { background: var(--ok); }
.infographic-delegconcept .switch .knob {
position: absolute; top: 0.18rem; left: 0.18rem; width: 1.44rem; height: 1.44rem; border-radius: 50%;
background: #fff; transition: transform 0.3s cubic-bezier(.4,1.4,.6,1); box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.infographic-delegconcept .switch.is-on .knob { transform: translateX(1.6rem); }
/* Hallway visual */
.infographic-delegconcept .hallway {
border: 2px dashed var(--line); border-radius: 10px; padding: 1.5rem 1rem 1rem; position: relative;
}
.infographic-delegconcept .hallway-label {
position: absolute; top: -0.7rem; left: 50%; transform: translateX(-50%);
font-family: var(--mono-font); font-size: 0.68rem; color: var(--ink-soft); background: var(--paper); padding: 0 0.5rem;
}
.infographic-delegconcept .desk {
display: flex; justify-content: center; margin-bottom: 1.2rem; min-height: 2.4rem; align-items: center;
}
.infographic-delegconcept .desk-guard {
font-family: var(--mono-font); font-size: 0.78rem; font-weight: 700; padding: 0.4rem 0.9rem; border-radius: 999px;
background: var(--ok-soft); color: var(--ok); border: 2px solid var(--ok);
opacity: 0; transform: scale(0.7); transition: opacity 0.3s ease, transform 0.3s ease;
}
.infographic-delegconcept .desk-guard.is-visible { opacity: 1; transform: scale(1); }
.infographic-delegconcept .doors-row { display: flex; gap: 0.6rem; justify-content: center; flex-wrap: wrap; }
.infographic-delegconcept .door {
position: relative; width: 4.6rem; height: 4.2rem; border-radius: 8px; border: 2px solid var(--line);
background: var(--paper-alt); display: flex; align-items: flex-end; justify-content: center;
font-family: var(--mono-font); font-size: 0.68rem; color: var(--ink-soft); padding-bottom: 0.35rem;
transition: transform 0.2s ease;
}
.infographic-delegconcept .door.pop-in { animation: dc-pop 0.35s ease; }
@keyframes dc-pop { 0% { transform: scale(0.5); opacity: 0; } 60% { transform: scale(1.08); } 100% { transform: scale(1); opacity: 1; } }
.infographic-delegconcept .door-guard {
position: absolute; top: -0.6rem; left: 50%; transform: translateX(-50%);
font-size: 0.85rem; opacity: 0; transition: opacity 0.3s ease;
}
.infographic-delegconcept .door-guard.is-visible { opacity: 1; }
.infographic-delegconcept .door-status {
position: absolute; bottom: -1.3rem; left: 50%; transform: translateX(-50%); white-space: nowrap;
font-family: var(--mono-font); font-size: 0.62rem; font-weight: 700; opacity: 0; transition: opacity 0.3s ease;
}
.infographic-delegconcept .door-status.is-visible { opacity: 1; }
.infographic-delegconcept .door-status.bad-tone { color: var(--bad); }
.infographic-delegconcept .door-status.ok-tone { color: var(--ok); }
.infographic-delegconcept .add-row { display: flex; justify-content: center; margin-top: 2rem; }
.infographic-delegconcept .add-btn {
font: inherit; font-size: 0.82rem; font-weight: 700; border: none; background: var(--ink);
color: var(--paper); padding: 0.55rem 1.2rem; border-radius: 999px; cursor: pointer; font-family: var(--mono-font);
}
.infographic-delegconcept .add-btn:hover { opacity: 0.88; }
.infographic-delegconcept .caption {
text-align: center; font-size: 0.83rem; color: var(--ink-soft); margin-top: 1.25rem; min-height: 1.3em;
}
.infographic-delegconcept .caption strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-delegconcept *, .infographic-delegconcept *::before, .infographic-delegconcept *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="ig-inner">
<h2 class="ig-title">Event Delegation</h2>
<div class="toggle-row">
<span class="toggle-label without active" id="labelWithout">a guard per door</span>
<div class="switch" id="switch"><div class="knob"></div></div>
<span class="toggle-label with" id="labelWith">one guard, one desk</span>
</div>
<div class="hallway">
<span class="hallway-label">the parent</span>
<div class="desk"><span class="desk-guard" id="deskGuard">๐ฎ watching everything</span></div>
<div class="doors-row" id="doorsRow"></div>
</div>
<div class="add-row">
<button class="add-btn" id="addDoorBtn">+ Add a new door</button>
</div>
<p class="caption" id="caption">Every door needs its own guard โ and a new one won't have anybody watching it.</p>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-delegconcept')
|| document.querySelector('.infographic-delegconcept');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') {
dark = true;
} else if (bodyAttr === 'false') {
dark = false;
} else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
const switchEl = wrapper.querySelector('#switch');
const labelWithout = wrapper.querySelector('#labelWithout');
const labelWith = wrapper.querySelector('#labelWith');
const deskGuard = wrapper.querySelector('#deskGuard');
const doorsRow = wrapper.querySelector('#doorsRow');
const addDoorBtn = wrapper.querySelector('#addDoorBtn');
const caption = wrapper.querySelector('#caption');
let isWith = false;
let doorCount = 3;
const MAX = 6;
function buildDoor(n, justAdded) {
const door = document.createElement('div');
door.className = 'door' + (justAdded ? ' pop-in' : '');
door.innerHTML = `
<span class="door-guard">๐ฎ</span>
Door ${n}
<span class="door-status"></span>
`;
return door;
}
function renderDoors() {
doorsRow.innerHTML = '';
for (let i = 1; i <= doorCount; i++) {
doorsRow.appendChild(buildDoor(i, false));
}
syncMode();
}
function syncMode() {
deskGuard.classList.toggle('is-visible', isWith);
wrapper.querySelectorAll('.door-guard').forEach(g => g.classList.toggle('is-visible', !isWith));
}
function switchMode(nextIsWith) {
isWith = nextIsWith;
switchEl.classList.toggle('is-on', isWith);
labelWith.classList.toggle('active', isWith);
labelWithout.classList.toggle('active', !isWith);
doorCount = 3;
renderDoors();
caption.innerHTML = isWith
? `One guard at the parent's desk checks every visitor โ old doors and new ones.`
: `Every door needs its own guard โ and a new one won't have anybody watching it.`;
}
addDoorBtn.addEventListener('click', () => {
if (doorCount >= MAX) return;
doorCount++;
const door = buildDoor(doorCount, true);
doorsRow.appendChild(door);
const guard = door.querySelector('.door-guard');
const status = door.querySelector('.door-status');
if (isWith) {
setTimeout(() => {
status.classList.add('is-visible', 'ok-tone');
status.textContent = 'โ covered';
caption.innerHTML = `<strong>Door ${doorCount}</strong> showed up โ the desk guard covers it automatically`;
}, 300);
} else {
setTimeout(() => {
status.classList.add('is-visible', 'bad-tone');
status.textContent = 'โ no guard!';
caption.innerHTML = `<strong>Door ${doorCount}</strong> has no guard โ someone has to remember to add one`;
}, 300);
}
});
switchEl.addEventListener('click', () => switchMode(!isWith));
labelWithout.addEventListener('click', () => switchMode(false));
labelWith.addEventListener('click', () => switchMode(true));
renderDoors();
switchMode(false);
})();
</script>.demo {
font-family: system-ui, sans-serif;
padding: 16px;
}
.demo h1 {
color: #2563eb;
margin: 0 0 8px;
}
.demo p {
color: #4b5563;
margin: 0;
}document.querySelector('.demo h1')?.addEventListener('click', () => {
alert('Hello from live preview!');
});