jQuery Event Delegation: When .on() Should Take a Selector Argument
Binding click handlers to rows breaks as soon as new rows arrive. Here is how jQuery's .on() selector argument delegates events, plus where delegation costs you.
23 May 2026, 20:40 UTC

When new rows stop responding
A list renders on page load with a delete button in each row. You wire it up with $('.row .delete').on('click', removeRow) and it works. Later the page appends a row from an AJAX response, and its delete button does nothing. Nothing is wrong with the AJAX code: the handler was bound to the elements that existed when that line ran, and the new row was not one of them.
The fix is to stop binding to the rows and bind to something that never gets replaced. jQuery's .on() supports this directly through its optional selector argument, and the pattern is called event delegation.
What the selector argument in .on() changes
With two arguments, .on(type, handler), jQuery attaches a listener to each element in the current set. With three, .on(type, selector, handler), it attaches one listener to the current set and runs the handler only when the event's target — or one of its ancestors up to the delegate — matches selector. That signature has existed since jQuery 1.7; older code used .delegate() or the deprecated .live(). jQuery 3.x is the current line, and the behavior described here is consistent across 1.7 through 3.x.
Two DOM mechanics make it work:
- Bubbling. A click on a button inside a row fires on the button, then the row, then the list, and onward toward
document. The delegate sits somewhere on that path. - Target matching. jQuery compares
event.targetand its ancestors against the selector, stopping at the delegate element. Inside the handler,thisis the matched element, not the delegate.
Because the listener lives on the container, elements added later are covered automatically. There is no re-binding step after .append() or .html().
Worked example: a task list that grows
Run the script below in a page that has already loaded jQuery, with the markup present. No special permissions are involved — this is ordinary page-script execution in the browser.
<div id="task-list"></div>
<button id="add-task">Add task</button>
// One listener on the container, filtered by the child selector.
$('#task-list').on('click', '.task-item', function () {
$(this).toggleClass('completed');
});
// Add a row after the listener is bound.
$('#add-task').on('click', function () {
$('#task-list').append('<div class="task-item">New task</div>');
});
Expected checks:
- Click "Add task", then click the new row. The
completedclass toggles, which shows the delegated handler ran for an element that did not exist at bind time. - In DevTools, select a
.task-itemand open the Event Listeners pane. The click listener is listed on#task-list, not on the row. - Click an empty part of the container. Nothing happens, because the target does not match
.task-item.
If step 1 fails, check that #task-list exists in the DOM at the moment .on() runs. Binding to a selector that matches nothing fails silently — no error, just a listener that was never attached.
Where delegation costs you
| Situation | Why it bites | Practical response |
|---|---|---|
| Non-bubbling events | Native focus and blur do not bubble, so a delegated handler will not observe them. |
Use focusin and focusout, which do bubble. jQuery's internal special-event handling for delegated focus/blur has varied by version, so confirm against the version you ship. |
Delegating at document |
Every click anywhere runs the selector match against the target's ancestor chain. | Delegate to the nearest container that survives re-rendering, such as the list wrapper. |
| Nested elements matching one selector | jQuery walks from the target up to the delegate and runs the handler once per matching element, so an inner and an outer match can both fire it. | Use a more specific selector, or branch on event.target inside the handler. |
An inner handler calling event.stopPropagation() |
The event never reaches the delegate, so the delegated handler silently stops running. | Remove the stop, or attach the outer behavior at the same level as the inner one. |
Deciding per case
Delegation is not automatically the better choice. For a fixed set of a few elements that are never replaced, direct binding is simpler to read and the memory argument does not apply. Delegation earns its place when the set of children is large, changes after load, or both.
A concrete audit you can run today: search your JavaScript for .on( calls that sit inside functions which also call .append(, .html(, or load remote fragments. Each of those is a candidate for conversion to a delegated listener on the nearest stable parent. Convert one, then verify with the three checks above — the class toggle on a freshly added row, the Event Listeners pane showing the listener on the container, and no firing on non-matching clicks.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.