Can YOU predict `event.target` for click events in each browser?

February 16, 2021 /

A while ago I fixed a frustrating fun bug involving browser-specific behavior. The behavior differences occurred with a combination of click events, input elements, and event.target.

I thought the root cause was pretty interesting, so here's a short blog post about it!

What was the bug?

To start off, imagine an app that looks something like this screenshot. The relevant app behavior is:

  • "Create post" is a modal overlay on top of the page.
  • Clicking outside of the modal will close it.
  • The "Content" textarea will stay vertically expanded while it has focus (a UI decision we made).

A modal overlay with two text fields, labelled "Content" and "Tags."

Seems simple enough, right? But there was a bug—in some cases, clicking the input labelled "Tags" would cause the modal to close.

Reproduction steps:

  • Use Chrome or Edge (Bug doesn't occur in Firefox or Safari!)
  • Starting editing the textarea labelled "Content."
  • Click the "Tags" input.

Expected result: The "Tags" input focuses:

A modal overlay, with the "Tags" input focused, and the "Content" textarea no longer expanhded vertically.

Actual result: The "Tags" input very briefly focuses, then the modal disappears:

Closed modal, showing page underneath

That's not what we want to happen! 😔

Why is this happening to me??

I started by finding the code that caused the modal to close. It looked something like this:

document.addEventListener("click", function onClick(event) {
  if (isElementOutsideOfModal(event.target)) {
    closeModal();
  }
});

You can think of this code as saying "whenever the user clicks outside of the modal, close the modal."

ℹ️ What does event.target refer to?

event.target refers to the specific element that the user clicked on. In this case, it could be anything in the document, such as a div or a button.

Don’t confuse it with event.currentTarget, which refers to the element that the event handler was attached to.

What did I actually click on?

It was odd to me that this code was closing the modal. I had clicked on the input element inside the modal, so why did the browser think I was clicking outside of the modal?

To investigate further, I logged event.target to see on which element the browser thought the click event occurred.

Console logs document click event.target is a div with id app.

It was #app, a container for most of the page. I was surprised, because I thought event.target would be the input element I had clicked on.

If you look closer at the repro steps, however, you may notice something. When I press the mouse button, it's on the input:

mouse cursor over the Tags input

But when I release the mouse button, it's outside of the modal, since the "Content" textarea only stays expanded while focused.

Mouse cursor is outside of the modal. It's in the same position as it was the last screenshot, but the modal got vertically shorter.

Huh! Looks like we found our "click outside of the modal."

How is event.target determined for click events?

Does that observation mean that event.target for click events is determined by where the mouse button is released? Not quite:

If the button is pressed on one element and the pointer is moved outside the element before the button is released, the event is fired on the most specific ancestor element that contained both elements.

MDN on the click event

Ok, so event.target is the common ancestor between the mouse "press" and "release" positions. That knowledge cleared up my understanding of why the modal got closed:

  • Mouse press causes the "Tags" input to focus.
  • Since focus moved off of the "Content" textarea, the textarea shrinks vertically, which shrinks the modal as well.
  • Next, the mouse release happens beneath the modal, on #app, firing a click event.
  • event.target is set to #app, the common ancestor between the "Tags" input and #app. (In this case, the common ancestor happens to be the same as the element we released the mouse on, but that's not always true.)
  • Since event.target is #app, which is outside of the modal, the modal gets closed.

That explanation solved most of the confusion for me, but I still had one more question...

Why did this bug only exist in Chrome/Edge?

I wanted to understand the specific browser differences that caused this bug to only exist in Chrome and Edge.

Exploration with a minimal working example

To get there, I started by making a demo app of the bug (try it out). With less code and a simpler app, it'd be easier to pinpoint the differences.

My demo app would show a red circle where I pressed the mouse, then another red circle where I released the mouse, then print event.target. It looked like this:

Mouse pressed and released in left div, event.target is left div

In that case, event.target was the left div, which makes sense. I pressed and released the mouse button in that same div, so the "common ancestor" of left and left is itself.

I tried it again, but I dragged the mouse from left to right before releasing:

Mouse pressed in left div, released in right div, event.target is container div

As we expected, event.target was container, the common ancestor of the left div (where I pressed the mouse) and the right div (where I released the mouse).

Chrome, Edge, Firefox, and Safari all had this same behavior. I was hoping to find a difference here, one that could explain the browser-specific bug I was fixing. Because I couldn't find a difference, it seemed like my "demo app" wasn't quite representative of the actual bug.

To make my demo app more accurate, I pulled in another detail from the actual bug I was trying to fix: Clicking on an input element instead of a div.

I added a left-input input element inside the left div.

Mouse pressed and released in left-input, event.target is left-input

Looks good so far.

Browser behavior differences

After that, I tried something closer to the original bug: Press the mouse on the left-input, then release it outside of the left container. Here is how it behaved in each browser:

Chrome

Chrome: mouse pressed in left-input, released in right div, event.target is container

Edge

Edge: mouse pressed in left-input, released in right div, event.target is container

Nothing unexpected so far.


Firefox

Firefox: mouse pressed in left-input, released in right div, event.target is left-input

Safari

Safari: mouse pressed in left-input, released in right div, no click event fired

Hmmmm.

It appears that event.target, specifically for input elements, behaves differently in Chrome and Edge than in Firefox and Safari.

That behavior difference is the source of our Chrome/Edge-only bug.

In Firefox, if you press the mouse button on an input element, event.target is always that input element. It doesn't matter where you release the mouse. Our "should modal close?" code then sees that event.target is within the modal, so it doesn't close the modal.

Safari doesn't even fire a click event at all, so it won't run the "should modal close?" code.

But in Chrome and Edge, event.target depends on where you release your mouse.

How can we fix it?

In this specific situation, we added a workaround. In the click handler, we'd check if an input was focused before closing the modal.

document.addEventListener("click", function onClick(event) {
  if (!isElementOutsideOfModal(document.activeElement)) {
    return;
  }

  if (isElementOutsideOfModal(event.target)) {
    closeContainer();
  }
});

It was a low-risk change we could ship quickly within legacy code, but I imagine there are plenty of other solutions we could explore.

Back to home / Discuss on Twitter / Source on GitHub / Report inaccuracy

something ↻ 2021 ??