Say you want to attach a click handler to a <button>
. You almost surely are, as outside of a <form>
, buttons don’t do anything without JavaScript. So you do that with something like this:
var button = document.querySelector("button");
button.addEventListener("click", function(e) {
// button was clicked
});
But that doesn’t use event delegation at all.
Event delegation is where you bind the click handler not directly to the element itself, but to an element higher up the DOM tree. The idea being that you can rip out and plop in new DOM stuff inside of there and not worry about events being destroyed and needing to re-bind them.
Say our button has a gear icon in it:
<button>
<svg>
<use xlink:href="#gear"></use>
</svg>
</button>
And we bind it by watching for clicks way up on the document element itself:
document.documentElement.addEventListener("click", function(e) {
});
How do we know if that click happened on the button or not? We have the target of the event for that:
document.documentElement.addEventListener("click", function(e) {
console.log(e.target);
});
This is where it gets tricky. In this example, even if the user clicks right on the button somewhere, depending on exactly where they click, e.target
could be:
- The button element
- The svg element
- The use element
So if you were hoping to be able to do something like this:
document.documentElement.addEventListener("click", function(e) {
if (e.target.tagName === "BUTTON") {
// may not work, because might be svg or use
}
});

Unfortunately, it’s not going to be that easy. It doesn’t matter if you check for classname or ID or whatever else, the element itself that you are expecting might just be wrong.
There is a pretty decent CSS fix for this… If we make sure nothing within the button has pointer-events, clicks inside the button will always be for the button itself:
button > * {
pointer-events: none;
}
This also prevents a situation where other JavaScript has prevented the event from bubbling up to the button itself (or higher).
document.querySelector("button > svg").addEventListener("click", function(e) {
e.stopPropagation();
e.preventDefault();
});
document.querySelector("button").addEventListener("click", function() {
// If the user clicked right on the SVG,
// this will never fire
});

Would it not just be better to bind events by id or class or some other data attribute? Then the DOM element itself wouldn’t matter, and it would help with separation of concerns a little more. Using CSS to help handle events seems, messy.
As far as safely handling delegated listeners, delegating an event handler is relatively easy with
closest
(and polyfilling the behavior is relatively straightforward by making use ofmatches
):I have a simple event delegation example on codepen
Just use
event.currentTarget
, which holds the element the eventListener was initially attached to. Makes life a lot easier once you get used to it ;)event.currentTarget
is awesome.Depending on the scenario, I generally use a combo of both currentTarget and pointer events: none. Though as soon as you come up with a rule, you find the next exception …
Yep, that’s what I’d do
I was always checking for
element.closest('.click-target-selector')
in the handler, but this is much better!Thanks!
@ashley sheridan
Sometimes if you need 1000 click events on table rows it’s better to have one single event with delegation (you don’t have to re-attach it when table contents change).
You could still let events bubble to all elements and then stop at the element you want to use the event for?
Another approach:
I solved the problem using CSS. I never use non-interactable elements for listening to DOM events, so I have only a few to worry about:
Works like a charm.
And while on the topic, the following prevents text selection when click-mashing interactable elements:
This closest() user is impressed. Thanks!
Or just use old school attribute ‘onclick’ if it is a simple case
I don’t understand this sentence: “The idea being that you can rip out and plop in new DOM stuff inside of there and not worry about events being destroyed and needing to re-bind them.”.
I’ve always been adding event listener on my button directly. Is that a problem ?
I believe he is referring to adding event listeners to dynamic elements. If you’re attaching a listener to a button that doesn’t yet exist on the DOM, event delegation is a way to handle it.
You could traverse up from the target node on, including the target node. If you hit a specific pattern (I’d prefer data-attributes) you got your observed target.
Example:
Now, you traverse up every click event from event.target until you find an attribute “data-clickhandler” or reach the root element you assigned the event handler to. Many clicks will traverse up to root without a match, but that doesn’t really slow modern engines down.
Once you found your target you can fetch the desired behaviour from the same data attribute. It’s up to you to find an appropriate design pattern. I’d prefer a simple lookup table.
This makes it even possible handle different events with the same target element:
Traversal isn’t too hard to implement in vanilla JS. JQuery has methods for it. Sadly prototype.js is not really appropriate anymore; They had a specific event method just for this strategy.
OR just use jQuery
jQuery doesn’t automatically help you here. Any stopped propagation on an inside element will screw you up in jQuery land just as much as anywhere else.
Great one Chris, I didn’t know about this one and I’m definitely going to use it!
Thanks.