Event Delegation · Astro Tech Blog

Event Delegation

Event delegation is a pattern that leverages bubbling to handle events efficiently. Instead of attaching a listener to every child element, you attach one listener to a parent and use event.target to figure out which child was clicked.

The Problem

<ul id="menu">
  <li>Home</li>
  <li>About</li>
  <li>Contact</li>
  <!-- 50 more items... -->
</ul>

Without delegation: attach 50+ listeners. With delegation: one listener on <ul>.

How Delegation Works

parent.addEventListener('click', function(event) {
  if (event.target.tagName === 'LI') {
    // handle the li click
  }
});
Demo: Event Delegation
HTML
<ul id='delegation-menu' style='list-style:none;padding:0;'>
<li style='padding:8px 12px;margin:4px 0;background:#eef2ff;border-radius:4px;cursor:pointer;border:1px solid #c7d2fe;'>Home</li>
<li style='padding:8px 12px;margin:4px 0;background:#eef2ff;border-radius:4px;cursor:pointer;border:1px solid #c7d2fe;'>About</li>
<li style='padding:8px 12px;margin:4px 0;background:#eef2ff;border-radius:4px;cursor:pointer;border:1px solid #c7d2fe;'>Contact</li>
<li style='padding:8px 12px;margin:4px 0;background:#eef2ff;border-radius:4px;cursor:pointer;border:1px solid #c7d2fe;'>Blog</li>
</ul>
<p><button id='add-item'>Add New Item</button></p>
<pre id='delegation-output' style='background:#f1f5f9;padding:8px;border-radius:4px;'></pre>
JavaScript
const menu = document.getElementById('delegation-menu');
const out = document.getElementById('delegation-output');

// One listener for ALL items (existing and future!)
menu.addEventListener('click', function(e) {
// Make sure we clicked an LI
const li = e.target.closest('li');
if (!li) return;
out.textContent = 'Clicked: ' + li.textContent + ' (handled by delegation)';
});

document.getElementById('add-item').onclick = function() {
const li = document.createElement('li');
li.style.cssText = 'padding:8px 12px;margin:4px 0;background:#eef2ff;border-radius:4px;cursor:pointer;border:1px solid #c7d2fe;';
const num = menu.children.length + 1;
li.textContent = 'Dynamic Item ' + num;
menu.appendChild(li);
out.textContent = 'Added: ' + li.textContent + ' — click it, it works!';
};
Live Output Window

Using closest() for Delegation

When you have nested elements inside a target, use closest() to find the right container:

table.addEventListener('click', function(e) {
  const row = e.target.closest('tr');
  if (!row) return;
  // handle row click
});
Demo: Delegation with closest()
HTML
<table id='delegate-table' border='1' style='border-collapse:collapse;width:100%;'>
<tr><th>Name</th><th>Action</th></tr>
<tr>
<td>Alice</td>
<td><button class='delete-btn' style='padding:4px 8px;cursor:pointer;'>Delete</button></td>
</tr>
<tr>
<td>Bob</td>
<td><button class='delete-btn' style='padding:4px 8px;cursor:pointer;'>Delete</button></td>
</tr>
<tr>
<td>Charlie</td>
<td><button class='delete-btn' style='padding:4px 8px;cursor:pointer;'>Delete</button></td>
</tr>
</table>
<pre id='delegate-output' style='background:#f1f5f9;padding:8px;border-radius:4px;'></pre>
JavaScript
const out = document.getElementById('delegate-output');

document.getElementById('delegate-table').addEventListener('click', function(e) {
const btn = e.target.closest('.delete-btn');
if (!btn) return;
const row = btn.closest('tr');
const name = row.cells[0].textContent;
row.remove();
out.textContent = 'Deleted: ' + name;
});
Live Output Window

Delegation with Data Attributes

Combine delegation with data-* attributes for clean, declarative handling:

Demo: Data Attributes + Delegation
HTML
<div id='action-panel'>
<button data-action='save' data-id='123' style='margin:4px;padding:8px 16px;cursor:pointer;'>Save</button>
<button data-action='edit' data-id='123' style='margin:4px;padding:8px 16px;cursor:pointer;'>Edit</button>
<button data-action='delete' data-id='123' style='margin:4px;padding:8px 16px;cursor:pointer;'>Delete</button>
<button data-action='archive' data-id='456' style='margin:4px;padding:8px 16px;cursor:pointer;'>Archive</button>
</div>
<pre id='action-output' style='background:#f1f5f9;padding:8px;border-radius:4px;'></pre>
JavaScript
const out = document.getElementById('action-output');

document.getElementById('action-panel').addEventListener('click', function(e) {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const action = btn.dataset.action;
const id = btn.dataset.id;
out.textContent = 'Action: ' + action + ' on item #' + id;
});
Live Output Window

Benefits of Delegation

BenefitWhy
PerformanceOne listener instead of hundreds
MemoryFewer closures, less memory usage
Dynamic contentWorks for elements added after page load
Cleaner codeCentralized event logic

When NOT to Use Delegation

Delegation isn’t always the right choice:

  • Events that don’t bubble (focus, blur, load, scroll, mouseenter)
  • When you need to stop propagation per-element
  • When different child types need completely different event logic
  • When you need event.preventDefault() on specific elements only

The matches() Alternative

Instead of closest(), you can use matches() for simpler cases:

parent.addEventListener('click', function(e) {
  if (e.target.matches('li')) {
    // handle
  }
});

But closest() is better when elements have nested content (e.g., an <a> or <span> inside <li>).

Key Takeaways

  • Event delegation uses bubbling to handle events at a parent level
  • Use event.target.closest(selector) to find the right child
  • Delegation works for dynamic elements added later
  • Great for performance with many similar elements
  • Avoid for events that don’t bubble or where per-element control is needed