Focus Events
Track when elements gain or lose focus for form validation, accessibility, and UI state management.
Event Reference
| Event | Fires When | Bubbles | Typical Elements |
|---|---|---|---|
onFocus | Element receives focus | Yes (unlike native focus) | <input>, <textarea>, <select>, <button>, <a>, any element with tabIndex |
onBlur | Element loses focus | Yes (unlike native blur) | Same as above |
onFocusCapture | Same as onFocus, but fires during capture phase | Capture | Same as above |
onBlurCapture | Same as onBlur, but fires during capture phase | Capture | Same as above |
React's
onFocusandonBlurbubble by default, matching the nativefocusin/focusoutbehavior -- not nativefocus/blurwhich do not bubble.
Recipe
Quick-reference recipe card -- copy-paste ready.
// Validate on blur, highlight on focus
function ValidatedInput() {
const [error, setError] = useState<string | null>(null);
const handleBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
const value = e.currentTarget.value.trim();
setError(value.length === 0 ? "This field is required" : null);
};
return (
<div>
<input
onFocus={() => setError(null)}
onBlur={handleBlur}
className={error ? "border-red-500" : "border-gray-300"}
/>
{error && <p className="text-red-500 text-sm mt-1">{error}</p>}
</div>
);
}When to reach for this: You need inline validation that runs after the user leaves a field, focus ring styling for accessibility, or tracking which element currently has focus.
Working Example
// components/ValidatedEmailField.tsx
"use client";
import { useState, useRef } from "react";
type FieldState = {
value: string;
touched: boolean;
error: string | null;
};
function validateEmail(email: string): string | null {
if (email.trim().length === 0) return "Email is required";
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return "Invalid email format";
return null;
}
export default function ValidatedEmailField() {
const [field, setField] = useState<FieldState>({
value: "",
touched: false,
error: null,
});
const [isFocused, setIsFocused] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const handleFocus: React.FocusEventHandler<HTMLInputElement> = () => {
setIsFocused(true);
};
const handleBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
setIsFocused(false);
const error = validateEmail(e.currentTarget.value);
setField((prev) => ({ ...prev, touched: true, error }));
};
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
const value = e.currentTarget.value;
setField((prev) => ({
...prev,
value,
// Re-validate on change only if the field was already touched
error: prev.touched ? validateEmail(value) : null,
}));
};
const ringClass = isFocused
? "ring-2 ring-blue-500 border-blue-500"
: field.error
? "border-red-500"
: "border-gray-300";
return (
<form
className="max-w-sm mx-auto p-6"
onSubmit={(e) => {
e.preventDefault();
const error = validateEmail(field.value);
if (error) {
setField((prev) => ({ ...prev, touched: true, error }));
inputRef.current?.focus();
return;
}
alert(`Submitted: ${field.value}`);
}}
>
<label htmlFor="email" className="block text-sm font-medium mb-1">
Email
</label>
<input
ref={inputRef}
id="email"
type="email"
value={field.value}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
aria-invalid={!!field.error}
aria-describedby={field.error ? "email-error" : undefined}
className={`w-full px-3 py-2 border rounded ${ringClass}`}
/>
{field.touched && field.error && (
<p id="email-error" role="alert" className="text-red-500 text-sm mt-1">
{field.error}
</p>
)}
<button
type="submit"
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Submit
</button>
</form>
);
}What this demonstrates:
- Validating on blur so users are not interrupted mid-typing
- Clearing errors on focus so the user gets a fresh start
- Re-validating on change only after the field has been touched once
- Using
aria-invalidandaria-describedbyfor screen reader accessibility - Programmatic focus via
inputRef.current?.focus()on submit error
Deep Dive
How It Works
- React wraps native
focusin/focusoutevents asonFocus/onBlur, which means they bubble through the React tree. This is intentional -- it lets a parent<form>or<div>listen for focus changes on any descendant. - The
FocusEventobject includesrelatedTarget, which references the element that is gaining focus (on blur) or losing focus (on focus). This lets you detect focus direction. - Capture-phase variants (
onFocusCapture,onBlurCapture) fire before the target element's handler, useful for intercepting focus in wrapper components.
Variations
Focus-within pattern (parent reacts to child focus):
function FieldGroup() {
const [hasFocusWithin, setHasFocusWithin] = useState(false);
return (
<div
onFocus={() => setHasFocusWithin(true)}
onBlur={(e) => {
// Only clear if focus is leaving the container entirely
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setHasFocusWithin(false);
}
}}
className={hasFocusWithin ? "ring-2 ring-blue-300 rounded p-4" : "p-4"}
>
<input placeholder="First name" className="block mb-2 border px-2 py-1" />
<input placeholder="Last name" className="block border px-2 py-1" />
</div>
);
}Auto-focus on mount:
function SearchModal() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// Focus after paint to avoid layout thrashing
requestAnimationFrame(() => {
inputRef.current?.focus();
});
}, []);
return <input ref={inputRef} placeholder="Search..." />;
}Focus trapping in modals:
function FocusTrap({ children }: { children: React.ReactNode }) {
const trapRef = useRef<HTMLDivElement>(null);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key !== "Tab") return;
const focusable = trapRef.current?.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (!focusable || focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
};
return (
<div ref={trapRef} onKeyDown={handleKeyDown}>
{children}
</div>
);
}Using relatedTarget to detect focus direction:
function DirectionalFocus() {
const handleBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
const leavingTo = e.relatedTarget as HTMLElement | null;
if (leavingTo?.dataset.cancel) {
// User tabbed to cancel -- discard changes
e.currentTarget.value = "";
}
};
return (
<div>
<input onBlur={handleBlur} placeholder="Type something" />
<button data-cancel="true">Cancel</button>
<button>Save</button>
</div>
);
}Blur with delay for dropdowns (prevent closing on option click):
function Dropdown() {
const [open, setOpen] = useState(false);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
const handleFocus = () => {
clearTimeout(timeoutRef.current);
setOpen(true);
};
const handleBlur = () => {
// Delay closing so click on dropdown option can fire first
timeoutRef.current = setTimeout(() => setOpen(false), 150);
};
return (
<div onFocus={handleFocus} onBlur={handleBlur}>
<input placeholder="Search..." />
{open && (
<ul className="border rounded mt-1 shadow">
<li className="px-3 py-1 cursor-pointer hover:bg-gray-100">Option A</li>
<li className="px-3 py-1 cursor-pointer hover:bg-gray-100">Option B</li>
</ul>
)}
</div>
);
}TypeScript Notes
// The generic parameter specifies the element type
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
e.currentTarget; // HTMLInputElement (always the element the handler is on)
e.target; // Element (could be a child that triggered the event)
};
// relatedTarget is typed as EventTarget | null
const handleBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
const next = e.relatedTarget as HTMLElement | null;
// Cast is needed because relatedTarget is EventTarget | null
if (next?.tagName === "BUTTON") {
// Focus moved to a button
}
};
// Using the shorthand type alias
const onFocus: React.FocusEventHandler<HTMLSelectElement> = (e) => {
// e is React.FocusEvent<HTMLSelectElement>
};
// When listening on a parent container for any child focus
const onContainerFocus = (e: React.FocusEvent<HTMLDivElement>) => {
// e.target may be an input, button, etc. inside the div
// e.currentTarget is always the div
};Gotchas
-
onBlurfires beforeonClick-- If you have a dropdown that closes on blur and options that useonClick, the blur fires first and unmounts the options before the click registers. Fix: UseonMouseDownwithe.preventDefault()on the option to prevent blur, or usesetTimeoutto delay the blur effect. -
relatedTargetisnullwhen focus moves outside the document -- When the user tabs out of the browser window or clicks on a non-focusable area,relatedTargetisnull. Fix: Always check fornullbefore accessing properties onrelatedTarget. -
React
onFocus/onBlurbubble, but nativefocus/blurdo not -- If you attach a nativefocuslistener viaaddEventListener, it will not bubble. Mixing native and React focus listeners leads to confusing behavior. Fix: Stick to React's synthetic events consistently, or use nativefocusin/focusoutif you must useaddEventListener. -
autoFocusprop causes focus beforeuseEffectruns -- TheautoFocusJSX prop focuses the element during the commit phase, before effects run. If your effect depends on knowing what is focused, it may see stale state. Fix: Use arefcallback orrequestAnimationFrameinsideuseEffectto check focus after paint. -
Calling
element.focus()during render causes React warnings -- Imperatively focusing during the render phase triggers side effects. Fix: Always call.focus()insideuseEffect, event handlers, orrequestAnimationFrame. -
Focus events fire on every child when using bubbling -- A parent
onFocushandler fires every time any focusable child gains focus, not just when focus enters the parent container. Fix: Usee.currentTarget.contains(e.relatedTarget)to distinguish "focus entered the container" from "focus moved between children." -
tabIndex={-1}makes elements focusable via JS but not Tab key -- SettingtabIndex={-1}allows.focus()calls but removes the element from the tab order. SettingtabIndex={0}adds it to the natural tab order. Fix: UsetabIndex={0}when you want keyboard-navigable elements,tabIndex={-1}only for programmatic focus targets.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
CSS :focus-within | You only need visual styling changes on parent when a child is focused | You need to run JavaScript logic on focus changes |
CSS :focus-visible | You want focus rings only for keyboard users, not mouse clicks | You need to track focus state in React state |
document.activeElement | You need to check what is currently focused at a point in time | You need reactive updates when focus changes |
FocusEvent via useEffect + addEventListener | You need capture-phase focus on document or window | React synthetic events already cover your use case |
| Headless UI / Radix focus management | You need production-grade focus trapping and restoration in modals | You have a simple single-field validation scenario |
FAQs
How does React's onFocus/onBlur differ from native focus/blur events?
React's onFocus and onBlur bubble through the React tree, matching the behavior of native focusin/focusout. Native focus/blur events do not bubble. This means a parent element can listen for focus changes on any descendant.
What is the relatedTarget property on focus events, and what does it tell you?
- On
onBlur,relatedTargetis the element that is gaining focus - On
onFocus,relatedTargetis the element that is losing focus - It is
nullwhen focus moves outside the document (e.g., user tabs to another window) - Always check for
nullbefore accessing properties on it
How do you implement a "focus-within" pattern where a parent reacts to any child gaining focus?
<div
onFocus={() => setHasFocusWithin(true)}
onBlur={(e) => {
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setHasFocusWithin(false);
}
}}
>
<input placeholder="First name" />
<input placeholder="Last name" />
</div>Gotcha: Why does my dropdown close before the option click registers?
onBlur fires before onClick. When your dropdown closes on blur, it unmounts the options before the click event fires. Fix this by using onMouseDown with e.preventDefault() on the options to prevent blur, or use setTimeout to delay closing.
What is the difference between tabIndex={0} and tabIndex={-1}?
tabIndex={0}adds the element to the natural tab order, making it keyboard-navigabletabIndex={-1}makes the element focusable via JavaScript (.focus()) but removes it from the tab order- Use
0for interactive elements users should reach via Tab; use-1for programmatic focus targets only
How do you validate a field on blur but clear the error on focus?
<input
onFocus={() => setError(null)}
onBlur={(e) => {
const value = e.currentTarget.value.trim();
setError(value.length === 0 ? "Required" : null);
}}
/>Gotcha: Why does autoFocus cause stale state in my useEffect?
The autoFocus prop focuses the element during the commit phase, before effects run. If your useEffect checks what is focused, it may see stale state. Use a ref callback or requestAnimationFrame inside useEffect to check focus after paint.
How do you implement focus trapping in a modal dialog?
Query all focusable elements inside the modal, then on Tab keydown redirect focus from the last element back to the first (and vice versa with Shift+Tab). Use querySelectorAll with the selector 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'.
When should you use CSS :focus-visible instead of React focus events?
- Use CSS
:focus-visiblewhen you only need visual styling (focus rings) for keyboard users, not mouse clicks - Use React
onFocus/onBlurwhen you need to run JavaScript logic or track focus state in React state - Use CSS
:focus-withinwhen you only need parent styling changes on child focus
Why does a parent onFocus fire repeatedly when focus moves between its children?
Because React's onFocus bubbles, it fires every time any focusable child gains focus. Use e.currentTarget.contains(e.relatedTarget as Node) to distinguish "focus entered the container" from "focus moved between children."
What is the correct TypeScript type for a focus event handler, and how do you type relatedTarget?
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
e.currentTarget; // HTMLInputElement
// relatedTarget is typed as EventTarget | null
const next = e.relatedTarget as HTMLElement | null;
if (next?.tagName === "BUTTON") { /* ... */ }
};How do you use the React.FocusEventHandler shorthand type in TypeScript?
const onFocus: React.FocusEventHandler<HTMLSelectElement> = (e) => {
// e is React.FocusEvent<HTMLSelectElement>
e.currentTarget; // HTMLSelectElement
};Related
- Keyboard Events -- Handling key presses for accessibility and shortcuts
- Form Events -- onChange, onSubmit, and controlled form patterns
- Mouse Events -- Click, hover, and pointer interactions