focus events are not a bubble , so you are right that the behavior in React is different from the behavior of the DOM. The DOM has a focusin event that makes a bubble ; here is a demo:
<div> <span onfocus="(function(){console.log('span focus')})()"> onfocus: <input type="text" onfocus="(function(){console.log('input focus')})()"> </span> </div> <div> <span onfocusin="(function(){console.log('span focusin')})()"> onfocusin: <input type="text" onfocusin="(function(){console.log('input focusin')})()"> </span> </div>
Looking through the React source code , it seems like it was intentional; The code checks to see if the browser supports the focus event with a capture and implements it through the focus event with ReactEventListener.trapCapturedEvent instead of ReactEventListener.trapBubbledEvent . This is necessary because React implements its synthetic event system using event delegation, and therefore it needs to use a capture or bubble for all event processing. An article related to the comment explains how this works:
The problem is that these events do not bubble. A focus or blur event on a link fires only on the link itself, and not on any element of the link's ancestor.
This is an ancient rule. Several events, primarily focus, blur and change, do not bubble up the document tree. The exact reasons for this were lost in the fog of history, but part of the reason is that these events simply do not make sense to some elements. The user cannot focus or change an arbitrary paragraph in any way, and therefore these events are simply not available for these HTML elements. In addition, they do not bubble.
...
Except when you use event capture.
...
One of the most interesting conclusions of my research on events is that when you define event handlers at the capture stage, the browser executes any event handlers installed on the ancestors of the event object, regardless of whether this event has the meaning of these elements or not.
It seems pretty likely that the React team decided to just make the event always a bubble (which, frankly, is what I expected from the DOM specification until I read your question). Browser implementations do not seem consistent; One comment on the question mentions that focus events are a bubble in Firefox, but I could not reproduce this in the latest version. However, using the onfocusin attribute or using addEventListener("focusin", ...) also did not work in FF. Perhaps it was just an attempt to normalize events in browsers.
All that has been said, it looks like there is an error in which the .bubbles property on SyntheticFocusEvent is false instead of true .