jQuery Event Handling: .on(), .click(), and Event Delegation
How I handle events in jQuery 1.9: .on() vs .click(), delegation for elements added later, this vs e.target, bubbling, namespaces, .off(), and the APIs that 1.9 removed.

The question I get: “My click handler works, but the buttons I add later do nothing. And I read that .live() is gone. What do I use now?” Both problems share one answer, and it is the most useful idea in jQuery event handling.
jQuery event handling means attaching functions to browser events such as clicks, key presses and form submits. jQuery makes them behave the same in every browser, including old Internet Explorer with its different event model. The official .on() documentation describes the full signature. I show the cases that matter here, with real output from jQuery 1.9.1, released in early February 2013. Here is the small fixture page:
<div id="app">
<ul id="todo"><li class="task"><span class="label">Write post</span> <a href="#del" class="del">x</a></li><li class="task"><span class="label">Test code</span> <a href="#del" class="del">x</a></li></ul>
<button id="add" type="button">Add task</button>
<a id="link" href="http://example.com/">link</a>
<div id="outer"><div id="inner"><button id="deep">deep</button></div></div>
</div>
My default: delegate first in jQuery event handling
My position: write .on() everywhere, and delegate by default inside any region of the page that changes. Direct binding is the special case. It suits elements that exist from the first render and never change. Most tutorials teach it the other way round. My default removes the whole class of “works on load, breaks after AJAX” bugs.
.on(), .click() and what changed in 1.9
Since jQuery 1.7, .on() is the single method of jQuery event handling for attaching handlers. The older .bind(), .delegate() and .live() all run on top of it. Shorthands such as .click(fn), .keyup(fn) and .submit(fn) still work and mean .on('click', fn). jQuery 1.9 removed several old APIs, and code that still uses them fails. This is real output from the 1.9.1 release:
console.log('jQuery', $.fn.jquery);
console.log('$.fn.live :', typeof $.fn.live);
console.log('$.fn.die :', typeof $.fn.die);
console.log('$.fn.bind :', typeof $.fn.bind);
console.log('$.fn.delegate :', typeof $.fn.delegate);
console.log('$.fn.on :', typeof $.fn.on);
console.log('$.browser :', typeof $.browser);
jQuery 1.9.1
$.fn.live : undefined
$.fn.die : undefined
$.fn.bind : function
$.fn.delegate : function
$.fn.on : function
$.browser : undefined
.live() and .die() are gone, along with $.browser. jQuery deprecated them in 1.7 and removed them in 1.9. .bind() and .delegate() still exist, so old code keeps working, but new code should use .on(). One more removal fails silently, so people miss it. It is the two-function form of .toggle() that alternated between two click handlers:
// In 1.8 and earlier .toggle(fn1, fn2) was a click-alternating helper; 1.9 removed it.
var calls = [];
$('#add').toggle(function () { calls.push('first'); }, function () { calls.push('second'); });
$('#add').click(); $('#add').click();
console.log('click twice ->', calls.join(', ') || '(handlers never ran)', '| #add visible state change?', $('#add').css('display'));
click twice -> (handlers never ran) | #add visible state change? inline-block
In 1.8 and earlier, that code alternated two handlers. In 1.9, jQuery no longer treats the function arguments as handlers, so neither ran. The call quietly became a “show or hide” call. If a toggle button stops responding after an upgrade, look here first. Replace it with a counter or a class check inside a normal click handler. Can’t change old code or plugins yet? The jQuery Migrate plugin restores the removed APIs and warns in the console when code uses them.
Direct handlers versus delegated handlers
var direct = 0, delegated = 0;
// Direct: attaches to the two <a class="del"> that exist right now
$('.del').on('click', function (e) { e.preventDefault(); direct += 1; });
// Delegated: ONE handler on the list, matched by selector at click time
$('#todo').on('click', '.del', function (e) { e.preventDefault(); delegated += 1; });
// A task added later
$('#todo').append('<li class="task"><span class="label">New</span> <a href="#del" class="del">x</a></li>');
$('.del').eq(0).click(); // old element
$('.del').eq(2).click(); // element added after binding
console.log('direct handler calls :', direct, '(the new element was missed)');
console.log('delegated handler calls:', delegated, '(both elements handled)');
direct handler calls : 1 (the new element was missed)
delegated handler calls: 2 (both elements handled)
This is the central example of jQuery event handling. The direct handler went onto the two .del links that existed at that moment. Then I added a third task, and clicking its link did nothing. So the direct handler ran once, for the first click only. The delegated handler sits on the list, and it ran for both clicks.
The reason is how browser events travel. A click on a link does not stop at the link. It bubbles up through every ancestor, all the way to the document. I explain that path in plain JavaScript in my post on event bubbling in JavaScript. A delegated handler listens on an ancestor and waits for clicks to arrive. When one arrives, it checks whether the clicked element matches your selector (.del). Because the check happens at click time, it covers elements that did not exist when you wrote the handler.
Delegation also uses far less memory
var html = '';
for (var i = 0; i < 500; i++) html += '<li class="row">row ' + i + '</li>';
$('#todo').html(html);
function countBound() {
var total = 0;
$('#todo, #todo .row').each(function () {
var ev = $._data(this, 'events');
if (ev && ev.click) total += ev.click.length;
});
return total;
}
$('#todo .row').on('click', function () {});
console.log('500 rows, direct binding -> click handlers in memory:', countBound());
$('#todo .row').off('click');
$('#todo').on('click', '.row', function () {});
console.log('500 rows, delegated binding-> click handlers in memory:', countBound());
500 rows, direct binding -> click handlers in memory: 500
500 rows, delegated binding-> click handlers in memory: 1
I read the count from jQuery’s internal data store. The $._data call is undocumented, so treat it as a measuring tool only. Binding directly to 500 rows kept 500 handlers in memory. Delegating from the list kept one. On a long list in an older browser, that decides whether the page responds or lags while loading.
What is this, and what is e.target?
$('#todo').on('click', '.del', function (e) {
console.log('this :', this.className);
console.log('e.target :', e.target.className);
console.log('e.currentTarget === this:', e.currentTarget === this);
console.log('e.delegateTarget.id :', e.delegateTarget.id);
console.log('e.type :', e.type);
e.preventDefault();
});
$('.del:first').click();
this : del
e.target : del
e.currentTarget === this: true
e.delegateTarget.id : todo
e.type : click
In a delegated handler, this (and e.currentTarget) is the element that matched the selector. Here that is the .del link, not the list that holds the handler. e.delegateTarget is the list. e.target is the deepest element the user clicked, which can be a child of the matched element. For a link that contains an icon <img>, e.target is the image and this is the link. I reach for this in nearly every case.
Bubbling, preventDefault and stopPropagation
var log = [];
$('#outer').on('click', function () { log.push('outer'); });
$('#inner').on('click', function (e) { log.push('inner'); });
$('#deep').on('click', function (e) { log.push('deep'); });
$('#deep').click();
console.log('bubbling order :', log.join(' > '));
log = [];
$('#deep').off('click').on('click', function (e) { log.push('deep'); e.stopPropagation(); });
$('#deep').click();
console.log('stopPropagation() :', log.join(' > '));
// return false = preventDefault() + stopPropagation()
var seen = {};
$('#link').on('click', function (e) { seen.before = e.isDefaultPrevented(); return false; });
var ev = $.Event('click');
$('#link').trigger(ev);
console.log('return false : defaultPrevented =', ev.isDefaultPrevented(), ', propagationStopped =', ev.isPropagationStopped());
bubbling order : deep > inner > outer
stopPropagation() : deep
return false : defaultPrevented = true , propagationStopped = true
Left alone, the click on the innermost button ran its own handler first, then its parent’s, then the outer one: deep > inner > outer. In jQuery event handling, two tools control this, and people often confuse them:
e.preventDefault()cancels the browser’s own default action, such as following a link or submitting a form. The event keeps bubbling.e.stopPropagation()stops the event travelling up to ancestors. The default action still happens.return falsein a jQuery handler does both, as the last output line shows. That is convenient but blunt. It also blocks every delegated handler higher up the tree. That explains many “dead” handlers elsewhere on the page.
Namespaces and removing handlers
var n = 0;
function handler() { n += 1; }
$('#add').on('click.report', handler).on('click.audit', handler);
$('#add').click();
console.log('two namespaced handlers :', n);
$('#add').off('click.report');
n = 0; $('#add').click();
console.log('after off(".report") :', n);
$('#add').off('.audit');
n = 0; $('#add').click();
console.log('after off(".audit") :', n);
$('#add').one('click', handler);
n = 0; $('#add').click(); $('#add').click();
console.log('one() fires only once :', n);
two namespaced handlers : 2
after off(".report") : 1
after off(".audit") : 0
one() fires only once : 1
Add a namespace after the event name, such as click.report. Later, .off('click.report') removes exactly that handler. .off('.audit') removes everything in the namespace. Other handlers on the same element stay put. Namespaces matter most in plugin code. Plugins should always namespace their handlers, so removing the plugin leaves your code alone. .one() attaches a handler that removes itself after the first run. A bare .off('click') removes every click handler on the element, including ones you did not write.
Custom events
$('#app').on('task:added', function (e, title, count) {
console.log(e.type, '->', title, count);
});
$('#add').on('click', function () {
// A custom event bubbles like a native one, so the listener on #app hears it
$(this).trigger('task:added', ['Ship it', 3]);
});
$('#add').click();
task:added -> Ship it 3
You are not limited to browser events. .trigger('task:added', [title, count]) fires an event with your own name. It passes extra arguments after the event object, and it bubbles like a click. So a listener on #app heard an event fired on the button inside it. Custom events decouple parts of a page. The code that adds a task announces it. The counter, the empty-state message and the save button each listen without knowing about each other.
Troubleshooting jQuery event handling
| Symptom | Cause | Fix |
|---|---|---|
| Handler works on load, not on elements added later | Direct binding | Delegate from a static ancestor |
Handler stops working after .html() replaces content |
The bound elements were destroyed | Delegate from the container |
$(...).live is not a function |
.live() was removed in 1.9 |
Use .on() with a delegate selector |
| Toggle button does nothing after upgrading | Two-function .toggle() was removed |
Track state in a class or variable |
| Handler fires twice | Attached twice, or inside another handler | Attach once; .off() before re-attaching |
| A handler elsewhere never fires | A descendant used return false |
Use preventDefault() only |
| Link jumps to top of page | href="#" default action |
e.preventDefault() |
How I structure jQuery event handling on real sites
- Delegate from a stable region. Lists, tables, menus and comment threads get one handler on the container. I delegate from the nearest static ancestor rather than
document. That way, not every click on the page runs through a selector check. When I replace those lists with jQuery append, prepend or html, the handlers keep working. - Namespace everything in a plugin. Use
.on('click.mywidget', …)and adestroymethod that calls.off('.mywidget'). The widget then stays removable. - Use data attributes as hooks.
data-action="delete"with a handler on[data-action]separates behaviour from styling classes. - Announce state changes with custom events. On larger pages I publish events like
cart:updatedinstead of calling other modules’ functions. - Guard against double submits. The handler disables the button, or uses
.one(). A double-click cannot send the form twice. For the checks that run before submit, see my jQuery form validation tutorial.
Two jQuery event handling reports reach me again and again. “The buttons stop working after the list reloads” is a direct handler on replaced content. “The toggle button does nothing after we upgraded jQuery” is the removed two-function .toggle(). The first needs a delegated handler on the container. The second needs a small click handler that checks a class. Since 1.9, I load jQuery Migrate in development for every upgrade. Its console warnings list each removed API the old code still calls.
Direct or delegated: how I decide
- Can the element be added, removed or replaced after load? If yes, delegate.
- Are there many similar elements? If yes, delegate: one handler beats hundreds.
- Is it a single permanent element (the main submit button)? Direct binding is fine and simpler.
- Which ancestor is stable? Choose the nearest one that is never replaced.
- Will it ever need removing? Add a namespace now.
When not to use delegation
- Events that do not bubble.
focusandblurdo not bubble in the browser. jQuery offersfocusinandfocusoutfor delegating those. - High-frequency events. A delegated
mousemoveon the whole document runs a selector check on every pixel of movement. Bind those directly, and keep the handler light. - When the handler must stop an event early. By the time a delegated handler calls
stopPropagation(), handlers on the child have already run.
Common jQuery event handling mistakes
- Binding directly to content that gets replaced. Delegate instead.
- Using
return falseby habit. It silently blocks other handlers. UsepreventDefault(). - Attaching handlers inside other handlers. Each click adds another copy. The same trap appears when you bind inside a loop, which I cover in my notes on jQuery .each() and $.each().
- Calling
.off('click')without a namespace. It removes other code’s handlers. - Still using
.live()or the two-function.toggle(). Both are gone in 1.9. - Delegating from
documentfor everything. Use the nearest static container.
FAQ
What is event delegation in jQuery?
Event delegation means one handler on a parent element. Clicks on its children bubble up to it, and jQuery checks whether the clicked element matches a selector. You write $('#list').on('click', '.item', fn). It works for elements added later and uses less memory than binding every child.
What is the difference between .on() and .click() in jQuery?
.click(fn) is shorthand for .on('click', fn) with a direct binding. .on() is more general. It accepts any event name, namespaces, several events at once, and a selector argument for delegation.
What replaced .live() in jQuery?
jQuery deprecated .live() in 1.7 and removed it in 1.9. Replace $('.x').live('click', fn) with $(parent).on('click', '.x', fn). Use the nearest ancestor that exists when the page loads. document works as the parent but runs slower on large pages.
Why does my jQuery click handler not work on dynamically added elements?
A direct handler only exists on elements that were in the page when you bound it. Elements added later have no handler, so jQuery event handling skips them. Bind a delegated handler to a stable ancestor, and clicks on new elements bubble up to it.
What is the difference between preventDefault, stopPropagation and return false?
preventDefault() cancels the browser default, such as following a link. stopPropagation() stops the event bubbling to ancestors. return false in a jQuery handler does both. I call the first two explicitly, because return false can silently block delegated handlers higher up.
Most event bugs are one bug: a handler on something that the page later replaced. Delegating from a stable parent removes that problem, and the page gets lighter as a bonus. My rule of thumb for jQuery event handling: if the elements can change, delegate from the nearest ancestor that cannot.
Originally posted on 12 February 2013 and post last modified on 2 November 2026