DOM Focus Event Order

Image of Author
October 12, 2022 (last updated October 18, 2022)

Browsers do not implement the specification order

The specification's event order disagrees strongly with most browser's event order. (Check updated date of this article in case it's changed!)

According to the spec, the order is as follows. Let A be the element with focus. Let B be the element to be focused.

  1. A is about to lose focus (A.focusout)
  2. B is about to gain focus (B.focusin)
  3. A has lost focus (A.blur)
  4. B has gained focus (B.focus)

Most browsers will order as follows:

  1. A loses focus (A.blur)
  2. A loses focus, repeat (A.focusout)
  3. B gains focus (B.focus)
  4. B gains focus repeat (B.focusin)

Why the repeats? Because focusin and focusout bubble, while focus and blur do not.

This is an unfortunate state of affairs because there is no interleaving events in most browsers. Interleaving blur/focus events would allow for simplified logic, aka an easier time programming, in certain circumstances.

A false negative in tracking group focus

Let's say you want to track combined focus of a few elements. You have a groupFocus boolean that tracks whether any group elements are focused, e.g., groupFocus = a.focus || b.focus.

At first you might think to do this by changing state on blur and focus events. E.g., a.onblur => a.focus = false, b.onfocus => b.focus = true, etc. This would lead to a brief false-negative after a.onblur flips a.focus to false. Both a and b are briefly in a false focus state. Eventually b.onfocus will run, but until then, any calculations based on groupFocus will lead to unexpected behaviors.

Spec order could fix this

One solution to this problem is via the spec's ordering, and using focusin and blur. They are ordered in a way that preserves the groupFocus boolean in the expected way. After b.onfocusin both a and b are true. Then, after a.blur, a is false but b stays true. Thus, groupFocus remains true the whole time.

FocusEvent.relatedTarget currently fixes this

For normal events there are usually two targets attached to the event object. event.currentTarget is the element that the event handler is attached to, and is the element that triggered the event. So a parent can catch a bubbling event from a child interaction. In such a situation, the parent is the currentTarget and the child is the target.

For focus events there's an additional target, event.relatedTarget. The related target corresponds to the "secondary" target being interacted with. On blur, it's the "other element" that's receiving the focus. On focus, it's the "other element" that is losing the focus.

With this, you can fix the false negative. On focusout you can check the relatedTarget. If it's in the group, groupFocus is true, otherwise, groupFocus is false.

React blur and focus

React blur and focus do bubble. This likely means they are actually using focusout and focusin internally, but I haven't confirmed. The real takeaway here is that you can setup a onBlur or onFocus on a parent element with the expectation of catching the event on bubble up.

For example, you can set an onBlur on a group wrapper div (with tabindex as well) and then catch any blurs. You can then check for containment: e.currentTarget.contains(e.relatedTarget). If it does, groupFocus is still true, otherwise false.

The future I hope for

I hope the browsers will implement focus event ordering as per the spec. This would mean there are meaningful ordering differences between all the events. React (and other frameworks) could then support all four focus event types.