Accessibility · Keyboard and Focus
The Order of Focus and Blur Events
Focus moves in a fixed sequence of four events, only two of which bubble. How relatedTarget identifies the other end of the move, and why calling focus() from inside a blur handler behaves unexpectedly.
A single focus move fires four events, in a fixed order, and two of them bubble. Components that listen for the wrong pair, or that assume the events arrive one at a time, produce state that is correct in the common case and wrong exactly when a widget is being torn down or replaced. The ordering is specified, and knowing it removes most of the guesswork from focus-related handlers.
Four events for one move
Moving focus from element A to element B fires, in order:
bluron A, non-bubblingfocusouton A, bubblingfocuson B, non-bubblingfocusinon B, bubbling
The two pairs are the same events at different propagation scopes. blur and focus do not bubble;
focusout and focusin do, and they are the pair to use for delegation. Listening for focus on a
container to catch focus entering any descendant does not work, because the event never reaches the
container.
All four are FocusEvent instances and all four carry a relatedTarget. On blur and focusout
the related target is the element gaining focus; on focus and focusin it is the element that lost
it. A handler can therefore decide whether a move is internal to a widget by comparing
relatedTarget against the widget’s own subtree — which is the correct way to distinguish “focus
moved between two items of this list” from “focus left the list”.
// Delegated focus tracking that survives items being added and removed.
list.addEventListener('focusin', (event) => {
const item = event.target.closest('[data-item]');
if (!item) return;
const cameFromInside = event.relatedTarget && list.contains(event.relatedTarget);
if (!cameFromInside) list.dataset.entered = 'true'; // first entry from outside the list
highlight(item);
});
list.addEventListener('focusout', (event) => {
const goingTo = event.relatedTarget;
if (goingTo && list.contains(goingTo)) return; // still inside: not an exit
clearHighlight(); // focus left the widget entirely
});
Both relatedTarget reads are null-safe because the value is null in cases where the other end is
not an element: focus moving to or from the document body, focus leaving the window, and focus moving
into or out of a shadow tree where retargeting produces null in some engines. A handler that
dereferences relatedTarget unconditionally throws on the most ordinary transition there is — the
first Tab press into the page.
Reentrancy: focus() inside blur
The events are dispatched synchronously during the focus change, which means a handler can start another focus change before the first has finished dispatching. The specified behaviour is that the focusing steps run again, and the second move’s events are dispatched from within the first move’s dispatch.
The practical hazard is a blur handler that validates a field and focuses the next one on success.
The sequence becomes: blur on A → handler calls focus() on B → focus and focusin on B
dispatch → the original blur dispatch resumes → focusout on A → focus on B again? No: the
second focus() call is a no-op because B is already focused, so the remaining events for the first
move continue after the second move has completed. The observable order is interleaved, and a state
machine that assumes the four events always arrive as a contiguous block breaks.
The safe pattern is to defer the second move out of the dispatch:
field.addEventListener('blur', (event) => {
const next = event.relatedTarget;
if (next) return; // the user is moving somewhere specific; leave it alone
queueMicrotask(() => {
if (document.activeElement === document.body) field.focus(); // restore only if focus was lost
});
});
The relatedTarget check is doing real work: it distinguishes “focus went somewhere” from “focus
went nowhere”, which is the case a handler should intervene in. Without it, a handler that
re-focuses on every blur makes the field impossible to leave.
Microtask timing matters here. A microtask runs before the next task and after the current dispatch
completes, so it observes the final activeElement without being inside the event. A
setTimeout(…, 0) would also work and would additionally allow rendering to occur in between, which
is sometimes needed when the target element is being unhidden in the same turn.
What the events do not tell you
focus and focusin fire when an element becomes focused, and there is no event for “focus was
attempted and refused”. A focus() call on a disabled control, an inert subtree, or a
non-focusable element produces no events at all. Code that waits for a focus event to confirm that
a programmatic move succeeded waits forever. Reading back document.activeElement is the only
confirmation available, as described in Which Elements Can Actually Take
Focus.
Focus events also do not fire for every change in document.activeElement. Moving focus inside a
shadow tree changes ShadowRoot.activeElement without changing document.activeElement, so a
document-level listener sees the host as focused throughout and receives no events for the internal
moves. The same applies to moves inside an embedded frame, which fire events in the frame’s document
only. Focus Across Shadow Roots and Frame
Boundaries covers the
indirection those boundaries introduce.
Window-level focus is a different pair of events entirely. window.focus and window.blur fire when
the whole browsing context gains or loses focus — switching tabs, switching applications, opening a
native dialog. They are not FocusEvents and carry no relatedTarget. A page that pauses an
animation on blur and resumes on focus reacts to tab switches, which is usually the intent; a
page that treats window.blur as “the user left the form” is wrong, because it also fires when the
user clicks the address bar.
Nor do the events tell you what the user’s pointer did. A click that focuses an element fires the
same four events as a Tab press, so anything that must distinguish keyboard from pointer navigation
has to record the modality from the triggering keydown or pointerdown rather than infer it from
the focus event. That recorded modality is also the input to the indicator heuristics described in
Focus Rings, :focus-visible, and the Heuristic Behind
Them.
Selection, caret, and the ordering of input events
Focus arrives before the first keystroke, which is what makes keydown handlers able to read
document.activeElement and find the intended target. The ordering is: pointer press → focus events
→ pointer release → click. Keyboard activation is: keydown → focus is already in place → keyup →
click for buttons and links that synthesise a click.
For text inputs the sequence continues with beforeinput and input after the key event. A handler
on keydown that moves focus cancels the character from being inserted only if it calls
preventDefault(), because the default action of the key event is the text insertion. Handlers that
move focus without preventing the default get one stray character inserted into the element that is
about to lose focus — the classic symptom of an input that advances on Enter and leaves a newline
behind, or of a search field that submits on a keystroke.
Composition adds a layer. During IME composition, keydown reports isComposing as true and the
key value is often "Process". A handler that advances focus on keydown must ignore events
during composition, or it interrupts a multi-keystroke character. The reliable test is
event.isComposing, not the key value.
Designing handlers around the order
Three rules follow from the ordering and cover most component needs.
Use focusin and focusout for anything at container level, and reserve focus and blur for
listeners attached directly to the element of interest. A component that needs to know about its own
descendants must use the bubbling pair.
Treat relatedTarget as the primary source of truth about where focus is going. Comparing it against
the container’s subtree answers the exit/entry question that would otherwise require timers or
flags, and it does so synchronously, at the moment the move happens.
Never mutate focus synchronously inside a handler that is itself reacting to a focus change, unless the reentrancy is understood and bounded. Deferring to a microtask keeps the four-event sequence intact for every other listener on the page, including those belonging to unrelated components that happen to be listening on the same document.
None of the four focus events is cancelable, so preventDefault() in a blur or focusout handler
does nothing and the move proceeds. There is no way to veto a focus change from an event handler;
the only mechanisms that prevent one are structural — making the destination not focusable, or
marking a subtree inert. This is why focus containment is built from inert and from key handling
rather than from event cancellation, and why a widget that needs focus to stay in one place arranges
for there to be nowhere else for it to go, as the active-descendant pattern does by leaving the
container as the only focusable element in the subtree.