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.
Referencia de eventos
| 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.
Receta
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.
Ejemplo funcional
// 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:
- Validar al perder enfoque para no interrumpir al usuario mientras escribe
- Limpiar errores al obtener enfoque para que el usuario tenga un nuevo comienzo
- Re-validar al cambio solo después de que el campo haya sido tocado una vez
- Usar
aria-invalidyaria-describedbypara accesibilidad de lectores de pantalla - Enfoque programático a través de
inputRef.current?.focus()en error de envío
Análisis profundo
Cómo funciona
- React envuelve los eventos nativos
focusin/focusoutcomoonFocus/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. - El objeto
FocusEventincluyerelatedTarget, 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. - Las variantes de fase de captura (
onFocusCapture,onBlurCapture) se activan antes del manejador del elemento objetivo, útiles para interceptar enfoque en componentes envolventes.
Variaciones
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>
);
}Notas de TypeScript
// 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
};Trampa
-
onBlurse activa antes deonClick-- Si tienes un dropdown que se cierra al perder enfoque y opciones que usanonClick, el blur se activa primero y desmonta las opciones antes de que se registre el clic. Solución: UsaonMouseDowncone.preventDefault()en la opción para prevenir el blur, o usasetTimeoutpara retrasar el efecto del blur. -
relatedTargetesnullcuando 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,relatedTargetesnull. Solución: Siempre verifica si haynullantes de acceder a propiedades enrelatedTarget. -
React
onFocus/onBlurse propagan, pero los nativosfocus/blurno -- Si adjuntas un manejador nativo defocusa través deaddEventListener, 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 nativosfocusin/focusoutsi debes usaraddEventListener. -
El prop
autoFocuscausa enfoque antes de que se ejecuteuseEffect-- El prop JSXautoFocusenfoca 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 orequestAnimationFramedentro deuseEffectpara 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 deuseEffect, manejadores de eventos, orequestAnimationFrame. -
Los eventos de enfoque se activan en cada hijo cuando se usa propagación -- Un manejador
onFocuspadre se activa cada vez que cualquier hijo enfocable gana enfoque, no solo cuando el enfoque entra en el contenedor padre. Solución: Usae.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 -- ConfigurartabIndex={-1}permite llamadas.focus()pero elimina el elemento del orden de tabulación. ConfigurartabIndex={0}lo agrega al orden de tabulación natural. Solución: UsatabIndex={0}cuando quieras elementos navegables por teclado,tabIndex={-1}solo para objetivos de enfoque programático.
Alternativas
| 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 |
FAQs
¿Cómo se diferencia onFocus/onBlur de React de los eventos nativos focus/blur?
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.
¿Qué es la propiedad relatedTarget en eventos de enfoque y qué te dice?
- En
onBlur,relatedTargetes el elemento que está ganando enfoque - En
onFocus,relatedTargetes el elemento que está perdiendo enfoque - Es
nullcuando el enfoque se mueve fuera del documento (ej., el usuario tabula a otra ventana) - Siempre verifica si hay
nullantes de acceder a propiedades en él
¿Cómo implementas un patrón "focus-within" donde un padre reacciona a cualquier hijo ganando enfoque?
<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>Trampa: ¿Por qué mi dropdown se cierra antes de que se registre el clic de la opción?
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.
¿Cuál es la diferencia entre tabIndex={0} y tabIndex={-1}?
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ón- Usa
0para elementos interactivos a los que los usuarios deben llegar vía Tab; usa-1solo para objetivos de enfoque programático
¿Cómo validas un campo al perder enfoque pero limpias el error al obtener enfoque?
<input
onFocus={() => setError(null)}
onBlur={(e) => {
const value = e.currentTarget.value.trim();
setError(value.length === 0 ? "Required" : null);
}}
/>Trampa: ¿Por qué autoFocus causa estado obsoleto en mi useEffect?
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.
¿Cómo implementas trampa de enfoque en un diálogo modal?
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"])'.
¿Cuándo deberías usar CSS :focus-visible en lugar de eventos de enfoque de React?
- Usa CSS
:focus-visiblecuando solo necesites estilos visuales (anillos de enfoque) para usuarios de teclado, no clics de ratón - Usa React
onFocus/onBlurcuando necesites ejecutar lógica de JavaScript o rastrear el estado del enfoque en el state de React - Usa CSS
:focus-withincuando solo necesites cambios de estilos del padre al enfoque del hijo
¿Por qué el onFocus padre se activa repetidamente cuando el enfoque se mueve entre sus hijos?
Porque 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".
¿Cuál es el tipo de TypeScript correcto para un manejador de evento de enfoque, y cómo escribes el tipo de relatedTarget?
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") { /* ... */ }
};¿Cómo usas el alias de tipo abreviado React.FocusEventHandler en TypeScript?
const onFocus: React.FocusEventHandler<HTMLSelectElement> = (e) => {
// e es React.FocusEvent<HTMLSelectElement>
e.currentTarget; // HTMLSelectElement
};Relacionado
- Eventos de teclado -- Manejo de pulsaciones de teclas para accesibilidad y atajos
- Eventos de formulario -- onChange, onSubmit y patrones de formulario controlado
- Eventos de ratón -- Clic, hover e interacciones de puntero