Event delegation is a pattern where you attach a single event listener to a stable parent element and let it handle events from many child elements — including ones added to the DOM later — by relying on event bubbling. In jQuery you write $(parent).on('click', '.child', handler); in modern vanilla JavaScript you write parent.addEventListener('click', e => { const el = e.target.closest('.child'); ... }). The payoff is fewer listeners, lower memory use, and handlers that keep working for dynamically added elements.
This pattern was a headline jQuery feature in 2014. In 2026 you rarely need jQuery for it — the native DOM gives you Element.closest() and event.target, which do delegation just as cleanly. This guide shows the jQuery approach, the recommended vanilla equivalent, how bubbling and stopPropagation affect it, and when delegation is genuinely worth the indirection.
Key takeaways
- Delegation = one listener on a parent, not one per child; events bubble up to it.
- It is the clean way to handle dynamically added elements without re-binding.
- jQuery:
$(parent).on(event, childSelector, handler). ThechildSelectorargument is what makes.on()delegated instead of direct. - Vanilla (recommended in 2026):
parent.addEventListener(type, e => { const el = e.target.closest(childSelector); if (el && parent.contains(el)) { ... } }). .live()was removed in jQuery 1.9 and.delegate()is deprecated — always use.on().- Delegation shines for large lists and dynamic content; for a few fixed elements, direct listeners are simpler and fine.
What is event delegation, and why does it matter?
When you click a button, the browser fires the event on that exact element (the event.target), then the event bubbles upward through every ancestor — the row, the list, the body, the document. Delegation takes advantage of this: instead of wiring a listener onto each child, you put one listener on a shared ancestor and inspect event.target to decide what was actually clicked.
Two concrete wins:
- Performance and memory. A table with 1,000 rows needs 1,000 listeners with direct binding, but only one with delegation.
- Dynamic elements. Rows added after page load are invisible to listeners bound earlier. A delegated listener on the stable parent catches them automatically, because the new elements still bubble through that parent.
How do you delegate events in jQuery?
jQuery folds delegation into .on(). Pass a child selector as the second argument and the handler only runs when the event originated from a matching descendant — current or future:
// Direct binding: one listener per <li> that exists right now.
// New <li>s added later are NOT covered.
$('#list li').on('click', function () {
console.log('clicked:', $(this).text());
});
// Delegated binding: ONE listener on the stable #list parent.
// The '.item' selector filters which descendants trigger it.
// Works for current AND future .item elements.
$('#list').on('click', '.item', function () {
// `this` is the matched .item, not #list
console.log('clicked item:', $(this).text());
});Inside a delegated handler, this (and event.currentTarget in older builds) refers to the matched child, while event.delegateTarget is the element you bound to. Avoid the legacy APIs: .live() was removed in jQuery 1.9 and .delegate() is deprecated — .on() replaces both. If you are weighing whether to keep jQuery at all, see our take on jQuery's overview and future.
What is the modern vanilla JavaScript equivalent?
The native DOM has no selector argument on addEventListener, so you reproduce the filter with Element.closest(). closest() walks from event.target up the ancestor chain and returns the nearest element matching a CSS selector (or null). A guard with parent.contains() keeps you from reacting to events that bubbled in from outside:
const list = document.querySelector('#list');
list.addEventListener('click', (event) => {
const item = event.target.closest('.item');
// Ignore clicks outside a .item, or that bubbled from another tree.
if (!item || !list.contains(item)) return;
console.log('clicked item:', item.textContent);
});This is the recommended approach in 2026: no library, broad browser support, and the same dynamic-element benefit. It is the exact pattern we reach for on client builds — see our web development services.
How do bubbling, capture, and stopPropagation affect delegation?
Delegation depends on events reaching the parent, so it is worth knowing the phases:
- Bubbling (default). The event travels target → ancestors. Delegation listens here.
- Capturing. Set
{ capture: true }(or passtrueas the third arg) to listen on the way down instead. Useful for events that do not bubble. event.targetvsevent.currentTarget.targetis where the event started (the child);currentTargetis the element whose listener is running (the parent). Delegation lives on the gap between them.event.stopPropagation()halts bubbling. Call it on a child and the event never reaches your delegated parent listener — a common reason a delegated handler silently does nothing.- Non-bubbling events.
focus,blur, and some media events do not bubble. Use the bubbling variantsfocusin/focusout, or attach with capture, to delegate them.
For the specifics of which mouse and pointer interactions bubble well, see jQuery mouse events and touch events.
Direct binding vs event delegation: which should you use?
| Aspect | Direct binding | Event delegation |
|---|---|---|
| Listeners attached | One per element | One on the parent |
| Dynamic / future elements | Not covered; must re-bind | Covered automatically |
| Memory on large lists | Higher | Lower |
| Setup effort | Trivial for a few elements | One handler plus a target check |
| Best fit | Small, fixed sets of elements | Large lists, tables, dynamic UIs |
jQuery to vanilla JS: a quick translation map
| Task | jQuery | Vanilla JS (2026) |
|---|---|---|
| Delegate a click | $(p).on('click', '.item', fn) |
p.addEventListener('click', e => { const el = e.target.closest('.item'); ... }) |
| Identify the matched child | $(this) |
e.target.closest('.item') |
| Element the listener sits on | e.delegateTarget |
e.currentTarget |
| Stop bubbling | e.stopPropagation() |
e.stopPropagation() |
| Legacy APIs to drop | .live() (removed), .delegate() (deprecated) |
use .on() / addEventListener |
When is delegation overkill?
Delegation adds a layer of indirection, so it is not always the right call. For a handful of stable, always-present elements — a single submit button, a navbar toggle — a direct listener is clearer and just as fast. Reach for delegation when you have many similar elements, when the set changes at runtime, or when re-binding after every render would be wasteful. A useful rule: if you would otherwise loop over a NodeList to attach the same handler repeatedly, delegate instead.
Frequently Asked Questions
What is event delegation in jQuery?
Event delegation in jQuery attaches one event handler to a stable parent element and uses a child selector so the handler runs for matching descendants. You write $(parent).on('click', '.child', handler). Because events bubble from the child up to the parent, the same listener also handles child elements added to the DOM later, without re-binding.
How is delegated .on() different from direct .on()?
The difference is the second argument. $('.item').on('click', fn) binds directly to every .item that exists right now. $('#list').on('click', '.item', fn) binds once to #list and delegates: it filters bubbling events by the .item selector, so it also covers future .item elements. The delegated form scales better and survives DOM changes.
Why does event delegation work for dynamically added elements?
Because the listener is on a parent that already exists, not on the children. When you insert a new child and an event fires on it, the event bubbles up to the parent, where the delegated listener inspects event.target and runs if it matches. Direct listeners, by contrast, only exist on elements present at bind time, so new elements are missed.
What is the vanilla JavaScript equivalent of jQuery delegation?
Use addEventListener on the parent plus Element.closest() to filter: parent.addEventListener('click', e => { const el = e.target.closest('.item'); if (el && parent.contains(el)) { ... } }). closest() finds the nearest matching ancestor of the clicked element, and parent.contains() guards against events that bubbled in from outside. This needs no library and is the recommended approach in 2026.
Why might a delegated event handler not fire?
The usual cause is event.stopPropagation() called on a child element, which halts bubbling before the event reaches your delegated parent. Other causes: listening for a non-bubbling event such as focus or blur (use focusin / focusout instead), binding to a parent that is itself replaced on re-render, or a child selector that does not actually match the clicked element.
Do I still need jQuery for event delegation in 2026?
No. Native addEventListener, event.target, and Element.closest() cover delegation with broad browser support and no dependency. jQuery is still fine in legacy code, but for new work the vanilla pattern is lighter and just as readable. Removing jQuery where it only handled delegation is a common, low-risk modernization step.