Be Slightly Careful with Sub Elements of Clickable Things

Avatar of Chris Coyier
Chris Coyier on

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
});