jQuery .on() Delegation for Dynamic Elements: When It Helps and When to Drop It
How jQuery .on() delegation handles dynamically added elements, a concrete task list example, and when native addEventListener makes jQuery unnecessary.
20 Jan 2026, 19:22 UTC

The problem: click handlers that miss elements added later
Single-page UIs add rows, cards and buttons after the initial load. Binding a handler to each new node creates memory churn and misses future nodes. The practical takeaway is to bind once to a stable ancestor and let jQuery filter by selector, which works for elements that do not exist yet.
How .on() delegation works in jQuery 1.7+
jQuery introduced .on() in version 1.7 as the unified method for binding events, replacing .bind(), .live() and .delegate(). Delegation uses $(staticAncestor).on('event','dynamicSelector',handler) to handle elements added to the DOM after the handler is attached.
Delegated handlers rely on event bubbling. When an event reaches the static ancestor, jQuery checks if the original target matches dynamicSelector and calls the handler with this set to the matching element. Non-bubbling events such as focus and blur need jQuery's focusin/focusout normalization or direct binding.
jQuery 1.9 removed .live() and .die(), leaving .on() as the supported delegation approach across the 1.x, 2.x and 3.x lines. Confirm the page loads jQuery 1.7 or later by checking jQuery.fn.jquery in the browser console before using .on().
Worked example: delegated clicks on a task list
The example runs in a browser console on a page that loads jQuery >=1.7. No output is claimed as tested.
// HTML
<ul id="task-list"></ul>
<button id="add-task">Add</button>
// jQuery delegation bound to a stable container
$('#task-list').on('click', 'li', function() {
console.log('Task clicked', this.textContent);
});
// Add items dynamically
$('#add-task').on('click', function() {
$('#task-list').append('<li>New task</li>');
});
A single listener on #task-list covers every current and future li inside it. One delegated listener on a static ancestor uses less memory than attaching individual handlers to many short-lived dynamic elements.
Native alternative and when jQuery adds cost
Modern browsers support the same pattern without jQuery:
const list = document.getElementById('task-list');
list.addEventListener('click', e => {
if (e.target.matches('li')) {
console.log('Task clicked', e.target.textContent);
}
});
This uses EventTarget.addEventListener and Element.matches for selector filtering. If a bundle audit shows jQuery is used only for .on() delegation, the native pattern removes the library and its download cost.
Trade-offs and limitations to verify before shipping
- Bubbling dependency. Delegation fails for events that do not bubble unless jQuery normalizes them. Test focus, blur, mouseenter and mouseleave carefully.
- Selector scope. A broad dynamic selector can match unintended descendants. Scope the selector to the nearest stable container.
- Propagation blocking. Calling event.stopPropagation() on a descendant can block delegated handlers bound to ancestors.
- Bundle weight. Including jQuery solely for .on() adds weight that vanilla JavaScript delegation can avoid.
Actionable check before removing jQuery
- Open DevTools Console on the page and run jQuery.fn.jquery to confirm version >=1.7.
- Append a new button after load and verify a delegated click handler on document or a static container fires for it.
- Audit the bundle with webpack-bundle-analyzer or vite-bundle-analyzer to determine whether jQuery is used for anything besides event handling before keeping it.
- Replace a delegated handler with the native addEventListener pattern and verify identical behavior for the target events.
If the audit confirms jQuery is only used for delegation, replace the handlers and remove the dependency. Otherwise keep .on() for its normalized focusin/focusout behavior and consistent cross-version API.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.