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:

  1. blur on A, non-bubbling
  2. focusout on A, bubbling
  3. focus on B, non-bubbling
  4. focusin on 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.

References

  1. UI Events Specification — Focus events — W3C
  2. HTML Living Standard — Focusing steps — WHATWG
  3. MDN Web Docs — FocusEvent.relatedTarget — MDN