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.
- A is about to lose focus (A.focusout)
- B is about to gain focus (B.focusin)
- A has lost focus (A.blur)
- B has gained focus (B.focus)
Most browsers will order as follows:
- A loses focus (A.blur)
- A loses focus, repeat (A.focusout)
- B gains focus (B.focus)
- 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
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.focus to false. Both
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
blur. They are ordered in a way that preserves the
groupFocus boolean in the expected way. After
b are true. Then, after
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
event.target 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
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 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
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
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.