Eventos de enfoque
Rastrear cuándo los elementos ganan o pierden el enfoque para validación de formularios, accesibilidad y gestión del estado de la UI.
Busca en todas las páginas de la documentación
Rastrear cuándo los elementos ganan o pierden el enfoque para validación de formularios, accesibilidad y gestión del estado de la UI.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
| Evento | Se activa cuando | Se propaga | Elementos típicos |
|---|---|---|---|
onFocus | El elemento recibe enfoque | Sí (a diferencia del native focus) | <input>, <textarea>, <select>, <button>, <a>, cualquier elemento con tabIndex |
onBlur | El elemento pierde enfoque | Sí (a diferencia del native blur) | Igual que arriba |
onFocusCapture | Igual que onFocus, pero se activa durante la fase de captura | Captura | Igual que arriba |
onBlurCapture | Igual que onBlur, pero se activa durante la fase de captura | Captura | Igual que arriba |
React's
onFocusyonBlurse propagan de forma predeterminada, coincidiendo con el comportamiento nativo defocusin/focusout-- no los nativosfocus/blurque no se propagan.
Tarjeta de referencia rápida -- lista para copiar y pegar.
// Validar al perder enfoque, resaltar al obtener enfoque
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>
);
}Cuándo usarlo: Necesitas validación inline que se ejecute después de que el usuario abandone un campo, estilos de anillo de enfoque para accesibilidad, o rastrear qué elemento tiene actualmente el enfoque.
// 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-validar al cambio solo si el campo ya fue tocado
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>
);
}Lo que esto demuestra:
aria-invalid y aria-describedby para accesibilidad de lectores de pantallainputRef.current?.focus() en error de envíofocusin/focusout como onFocus/onBlur, lo que significa que se propagan a través del árbol de React. Esto es intencional -- permite a un componente padre como <form> o <div> escuchar cambios de enfoque en cualquier descendiente.FocusEvent incluye relatedTarget, que hace referencia al elemento que está ganando enfoque (al perder enfoque) o perdiendo enfoque (al obtener enfoque). Esto te permite detectar la dirección del enfoque.onFocusCapture, onBlurCapture) se activan antes del manejador del elemento objetivo, útiles para interceptar enfoque en componentes envolventes.Patrón focus-within (padre reacciona a enfoque de hijo):
function FieldGroup() {
const [hasFocusWithin, setHasFocusWithin] = useState(false);
return (
<div
onFocus={() => setHasFocusWithin(true)}
onBlur={(e) => {
// Solo limpiar si el enfoque está saliendo del contenedor completamente
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-enfoque al montar:
function SearchModal() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// Enfocar después del dibujado para evitar layout thrashing
requestAnimationFrame(() => {
inputRef.current?.focus();
});
}, []);
return <input ref={inputRef} placeholder="Search..." />;
}Trampa de enfoque en modales:
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>
);
}Usar relatedTarget para detectar la dirección del enfoque:
function DirectionalFocus() {
const handleBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
const leavingTo = e.relatedTarget as HTMLElement | null;
if (leavingTo?.dataset.cancel) {
// Usuario tabuló a cancelar -- descartar cambios
e.currentTarget.value = "";
}
};
return (
<div>
<input onBlur={handleBlur} placeholder="Type something" />
<button data-cancel="true">Cancel</button>
<button>Save</button>
</div>
);
}Perder enfoque con retraso para dropdowns (prevenir cierre al hacer clic en opción):
function Dropdown() {
const [open, setOpen] = useState(false);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
const handleFocus = () => {
clearTimeout(timeoutRef.current);
setOpen(true);
};
const handleBlur = () => {
// Retrasar el cierre para que el clic en la opción del dropdown se active primero
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>
);
}// El parámetro genérico especifica el tipo de elemento
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
e.currentTarget; // HTMLInputElement (siempre el elemento en el que está el manejador)
e.target; // Element (podría ser un hijo que activó el evento)
};
// relatedTarget está tipado como EventTarget | null
const handleBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
const next = e.relatedTarget as HTMLElement | null;
// Se necesita casting porque relatedTarget es EventTarget | null
if (next?.tagName === "BUTTON") {
// El enfoque se movió a un botón
}
};
// Usando el alias de tipo abreviado
const onFocus: React.FocusEventHandler<HTMLSelectElement> = (e) => {
// e es React.FocusEvent<HTMLSelectElement>
};
// Al escuchar en un contenedor padre para cualquier enfoque de hijo
const onContainerFocus = (e: React.FocusEvent<HTMLDivElement>) => {
// e.target podría ser un input, botón, etc. dentro del div
// e.currentTarget siempre es el div
};onBlur se activa antes de onClick -- Si tienes un dropdown que se cierra al perder enfoque y opciones que usan onClick, el blur se activa primero y desmonta las opciones antes de que se registre el clic. Solución: Usa onMouseDown con e.preventDefault() en la opción para prevenir el blur, o usa setTimeout para retrasar el efecto del blur.
relatedTarget es null cuando el enfoque se mueve fuera del documento -- Cuando el usuario tabula fuera de la ventana del navegador o hace clic en un área no enfocable, relatedTarget es null. Solución: Siempre verifica si hay null antes de acceder a propiedades en relatedTarget.
React onFocus/onBlur se propagan, pero los nativos focus/blur no -- Si adjuntas un manejador nativo de focus a través de addEventListener, no se propagará. Mezclar escuchadores de enfoque nativos y de React lleva a comportamientos confusos. Solución: Mantente con los eventos sintéticos de React de forma consistente, o usa los nativos focusin/focusout si debes usar addEventListener.
El prop autoFocus causa enfoque antes de que se ejecute useEffect -- El prop JSX autoFocus enfoca el elemento durante la fase de commit, antes de que se ejecuten los efectos. Si tu efecto depende de saber qué está enfocado, podría ver estado obsoleto. Solución: Usa un callback ref o requestAnimationFrame dentro de useEffect para verificar el enfoque después del dibujado.
Llamar a element.focus() durante el renderizado causa advertencias de React -- El enfoque imperativo durante la fase de renderizado activa efectos secundarios. Solución: Siempre llama a .focus() dentro de useEffect, manejadores de eventos, o requestAnimationFrame.
Los eventos de enfoque se activan en cada hijo cuando se usa propagación -- Un manejador onFocus padre se activa cada vez que cualquier hijo enfocable gana enfoque, no solo cuando el enfoque entra en el contenedor padre. Solución: Usa e.currentTarget.contains(e.relatedTarget as Node) para distinguir "el enfoque entró en el contenedor" de "el enfoque se movió entre hijos".
tabIndex={-1} hace elementos enfocables vía JS pero no la tecla Tab -- Configurar tabIndex={-1} permite llamadas .focus() pero elimina el elemento del orden de tabulación. Configurar tabIndex={0} lo agrega al orden de tabulación natural. Solución: Usa tabIndex={0} cuando quieras elementos navegables por teclado, tabIndex={-1} solo para objetivos de enfoque programático.
| Alternativa | Usarlo cuando | No usarlo cuando |
|---|---|---|
CSS :focus-within | Solo necesitas cambios de estilos visuales en el padre cuando un hijo está enfocado | Necesitas ejecutar lógica de JavaScript en cambios de enfoque |
CSS :focus-visible | Quieres anillos de enfoque solo para usuarios de teclado, no clics de ratón | Necesitas rastrear el estado del enfoque en el state de React |
document.activeElement | Necesitas verificar qué está actualmente enfocado en un punto en el tiempo | Necesitas actualizaciones reactivas cuando cambia el enfoque |
FocusEvent vía useEffect + addEventListener | Necesitas enfoque de fase de captura en document o window | Los eventos sintéticos de React ya cubren tu caso de uso |
| Headless UI / Radix focus management | Necesitas trampa de enfoque de grado de producción y restauración en modales | Tienes un escenario simple de validación de campo único |
El onFocus y onBlur de React se propagan a través del árbol de React, coincidiendo con el comportamiento de los nativos focusin/focusout. Los eventos nativos focus/blur no se propagan. Esto significa que un elemento padre puede escuchar cambios de enfoque en cualquier descendiente.
onBlur, relatedTarget es el elemento que está ganando enfoqueonFocus, relatedTarget es el elemento que está perdiendo enfoquenull cuando el enfoque se mueve fuera del documento (ej., el usuario tabula a otra ventana)null antes de acceder a propiedades en él<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>onBlur se activa antes de onClick. Cuando tu dropdown se cierra al perder enfoque, desmonta las opciones antes de que se active el evento click. Soluciona esto usando onMouseDown con e.preventDefault() en las opciones para prevenir el blur, o usa setTimeout para retrasar el cierre.
tabIndex={0} agrega el elemento al orden de tabulación natural, haciéndolo navegable por tecladotabIndex={-1} hace el elemento enfocable vía JavaScript (.focus()) pero lo elimina del orden de tabulación0 para elementos interactivos a los que los usuarios deben llegar vía Tab; usa -1 solo para objetivos de enfoque programático<input
onFocus={() => setError(null)}
onBlur={(e) => {
const value = e.currentTarget.value.trim();
setError(value.length === 0 ? "Required" : null);
}}
/>El prop autoFocus enfoca el elemento durante la fase de commit, antes de que se ejecuten los efectos. Si tu useEffect verifica qué está enfocado, podría ver estado obsoleto. Usa un callback ref o requestAnimationFrame dentro de useEffect para verificar el enfoque después del dibujado.
Consulta todos los elementos enfocables dentro del modal, luego en la pulsación de tecla Tab redirige el enfoque del último elemento de vuelta al primero (y viceversa con Shift+Tab). Usa querySelectorAll con el selector 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'.
:focus-visible cuando solo necesites estilos visuales (anillos de enfoque) para usuarios de teclado, no clics de ratónonFocus/onBlur cuando necesites ejecutar lógica de JavaScript o rastrear el estado del enfoque en el state de React:focus-within cuando solo necesites cambios de estilos del padre al enfoque del hijoPorque React's onFocus se propaga, se activa cada vez que cualquier hijo enfocable gana enfoque. Usa e.currentTarget.contains(e.relatedTarget as Node) para distinguir "el enfoque entró en el contenedor" de "el enfoque se movió entre hijos".
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
e.currentTarget; // HTMLInputElement
// relatedTarget está tipado como EventTarget | null
const next = e.relatedTarget as HTMLElement | null;
if (next?.tagName === "BUTTON") { /* ... */ }
};const onFocus: React.FocusEventHandler<HTMLSelectElement> = (e) => {
// e es React.FocusEvent<HTMLSelectElement>
e.currentTarget; // HTMLSelectElement
};Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥