Eventos de Foco
Acompanhe quando os elementos ganham ou perdem o foco para validação de formulários, acessibilidade e gerenciamento de estado da UI.
Busque em todas as páginas da documentação
Acompanhe quando os elementos ganham ou perdem o foco para validação de formulários, acessibilidade e gerenciamento de estado da UI.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
| Evento | Dispara Quando | Borbulha | Elementos Típicos |
|---|---|---|---|
onFocus | Elemento recebe foco | Sim (diferente do focus nativo) | <input>, <textarea>, <select>, <button>, <a>, qualquer elemento com tabIndex |
onBlur | Elemento perde foco | Sim (diferente do blur nativo) | Igual ao acima |
onFocusCapture | Igual a onFocus, mas dispara durante a fase de captura | Captura | Igual ao acima |
onBlurCapture | Igual a onBlur, mas dispara durante a fase de captura | Captura | Igual ao acima |
onFocuseonBlurdo React borbulham por padrão, correspondendo ao comportamento nativofocusin/focusout-- não aos nativosfocus/blurque não borbulham.
Cartão de receita de referência rápida -- pronto para copiar e colar.
// Valide ao perder o foco, destaque ao ganhar o foco
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 ? "Este campo é obrigatório" : 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>
);
}Quando usar isso: Você precisa de validação inline que é executada após o usuário sair de um campo, estilos de anel de foco para acessibilidade ou rastreamento de qual elemento tem o foco atualmente.
// 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 "O email é obrigatório";
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return "Formato de email inválido";
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,
// Revalida na mudança apenas se o campo já foi 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(`Enviado: ${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"
>
Enviar
</button>
</form>
);
}O que isso demonstra:
aria-invalid e aria-describedby para acessibilidade de leitores de telainputRef.current?.focus() em erro de enviofocusin/focusout como onFocus/onBlur, o que significa que eles borbulham através da árvore do React. Isso é intencional -- permite que um <form> ou <div> pai escute as mudanças de foco em qualquer descendente.FocusEvent inclui relatedTarget, que referencia o elemento que está ganhando foco (ao perder o foco) ou perdendo foco (ao ganhar o foco). Isso permite detectar a direção do foco.onFocusCapture, onBlurCapture) disparam antes do manipulador do elemento de destino, útil para interceptar o foco em componentes wrapper.Padrão focus-within (pai reage ao foco do filho):
function FieldGroup() {
const [hasFocusWithin, setHasFocusWithin] = useState(false);
return (
<div
onFocus={() => setHasFocusWithin(true)}
onBlur={(e) => {
// Limpa apenas se o foco estiver saindo completamente do contêiner
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setHasFocusWithin(false);
}
}}
className={hasFocusWithin ? "ring-2 ring-blue-300 rounded p-4" : "p-4"}
>
<input placeholder="Primeiro nome" className="block mb-2 border px-2 py-1" />
<input placeholder="Último nome" className="block border px-2 py-1" />
</div>
);
}Auto-foco ao montar:
function SearchModal() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// Foca após a pintura para evitar layout thrashing
requestAnimationFrame(() => {
inputRef.current?.focus();
});
}, []);
return <input ref={inputRef} placeholder="Pesquisar..." />;
}Captura de foco em modais:
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>
);
}Usando relatedTarget para detectar a direção do foco:
function DirectionalFocus() {
const handleBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
const leavingTo = e.relatedTarget as HTMLElement | null;
if (leavingTo?.dataset.cancel) {
// O usuário pressionou Tab para cancelar -- descarte as alterações
e.currentTarget.value = "";
}
};
return (
<div>
<input onBlur={handleBlur} placeholder="Digite algo" />
<button data-cancel="true">Cancelar</button>
<button>Salvar</button>
</div>
);
}Blur com atraso para dropdowns (evita fechar ao clicar na opção):
function Dropdown() {
const [open, setOpen] = useState(false);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
const handleFocus = () => {
clearTimeout(timeoutRef.current);
setOpen(true);
};
const handleBlur = () => {
// Atraso no fechamento para que o clique na opção do dropdown possa disparar primeiro
timeoutRef.current = setTimeout(() => setOpen(false), 150);
};
return (
<div onFocus={handleFocus} onBlur={handleBlur}>
<input placeholder="Pesquisar..." />
{open && (
<ul className="border rounded mt-1 shadow">
<li className="px-3 py-1 cursor-pointer hover:bg-gray-100">Opção A</li>
<li className="px-3 py-1 cursor-pointer hover:bg-gray-100">Opção B</li>
</ul>
)}
</div>
);
}// O parâmetro genérico especifica o tipo do elemento
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
e.currentTarget; // HTMLInputElement (sempre o elemento onde o manipulador está)
e.target; // Element (pode ser um filho que disparou o evento)
};
// relatedTarget é tipado como EventTarget | null
const handleBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
const next = e.relatedTarget as HTMLElement | null;
// O cast é necessário porque relatedTarget é EventTarget | null
if (next?.tagName === "BUTTON") {
// O foco mudou para um botão
}
};
// Usando o alias de tipo abreviado
const onFocus: React.FocusEventHandler<HTMLSelectElement> = (e) => {
// e é React.FocusEvent<HTMLSelectElement>
};
// Ao escutar em um contêiner pai para qualquer foco de filho
const onContainerFocus = (e: React.FocusEvent<HTMLDivElement>) => {
// e.target pode ser um input, button, etc. dentro do div
// e.currentTarget é sempre o div
};onBlur dispara antes de onClick -- Se você tem um dropdown que fecha ao perder o foco e opções que usam onClick, o blur dispara primeiro e desmonta as opções antes que o clique seja registrado. Correção: Use onMouseDown com e.preventDefault() na opção para evitar o blur, ou use setTimeout para atrasar o efeito de blur.
relatedTarget é null quando o foco se move para fora do documento -- Quando o usuário pressiona Tab para fora da janela do navegador ou clica em uma área não focável, relatedTarget é null. Correção: Sempre verifique se é null antes de acessar propriedades em relatedTarget.
onFocus/onBlur do React borbulham, mas focus/blur nativos não -- Se você anexar um listener focus nativo via addEventListener, ele não borbulhará. Misturar listeners de foco nativos e do React leva a um comportamento confuso. Correção: Mantenha os eventos sintéticos do React de forma consistente, ou use focusin/focusout nativos se precisar usar addEventListener.
A prop autoFocus causa foco antes que useEffect seja executado -- A prop autoFocus do JSX foca o elemento durante a fase de commit, antes que os efeitos sejam executados. Se seu efeito depende de saber o que está focado, ele pode ver um estado obsoleto. Correção: Use um callback de ref ou requestAnimationFrame dentro de useEffect para verificar o foco após a pintura.
Chamar element.focus() durante a renderização causa avisos do React -- Focar imperativamente durante a fase de renderização aciona efeitos colaterais. Correção: Sempre chame .focus() dentro de useEffect, manipuladores de eventos ou requestAnimationFrame.
Eventos de foco disparam em cada filho ao usar bubbling -- Um manipulador onFocus pai dispara toda vez que qualquer filho focável ganha foco, não apenas quando o foco entra no contêiner pai. Correção: Use e.currentTarget.contains(e.relatedTarget) para distinguir "foco entrou no contêiner" de "foco se moveu entre filhos".
tabIndex={-1} torna os elementos focáveis via JS, mas não pela tecla Tab -- Definir tabIndex={-1} permite chamadas .focus(), mas remove o elemento da ordem de tabulação. Definir tabIndex={0} o adiciona à ordem de tabulação natural. Correção: Use tabIndex={0} quando quiser elementos navegáveis pelo teclado, tabIndex={-1} apenas para destinos de foco programático.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
:focus-within do CSS | Você só precisa de mudanças visuais de estilo no pai quando um filho está focado | Você precisa executar lógica JavaScript em mudanças de foco |
:focus-visible do CSS | Você quer anéis de foco apenas para usuários do teclado, não cliques do mouse | Você precisa rastrear o estado de foco no estado do React |
document.activeElement | Você precisa verificar o que está focado no momento | Você precisa de atualizações reativas quando o foco muda |
FocusEvent via useEffect + addEventListener | Você precisa de captura de fase de foco em document ou window | Eventos sintéticos do React já cobrem seu caso de uso |
| Gerenciamento de foco Headless UI / Radix | Você precisa de captura e restauração de foco de nível de produção em modais | Você tem um cenário simples de validação de campo único |
onFocus e onBlur do React borbulham através da árvore do React, correspondendo ao comportamento de focusin/focusout nativos. Os eventos nativos focus/blur não borbulham. Isso significa que um elemento pai pode escutar mudanças de foco em qualquer descendente.
onBlur, relatedTarget é o elemento que está ganhando focoonFocus, relatedTarget é o elemento que está perdendo foconull quando o foco se move para fora do documento (por exemplo, o usuário pressiona Tab para outra janela)null antes de acessar propriedades nele<div
onFocus={() => setHasFocusWithin(true)}
onBlur={(e) => {
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setHasFocusWithin(false);
}
}}
>
<input placeholder="Primeiro nome" />
<input placeholder="Último nome" />
</div>onBlur dispara antes de onClick. Quando seu dropdown fecha ao perder o foco, ele desmonta as opções antes que o evento de clique seja disparado. Corrija isso usando onMouseDown com e.preventDefault() nas opções para evitar o blur, ou use setTimeout para atrasar o fechamento.
tabIndex={0} adiciona o elemento à ordem de tabulação natural, tornando-o navegável pelo tecladotabIndex={-1} torna o elemento focável via JavaScript (.focus()) mas o remove da ordem de tabulação0 para elementos interativos que os usuários devem alcançar via Tab; use -1 apenas para destinos de foco programático<input
onFocus={() => setError(null)}
onBlur={(e) => {
const value = e.currentTarget.value.trim();
setError(value.length === 0 ? "Obrigatório" : null);
}}
/>A prop autoFocus foca o elemento durante a fase de commit, antes que os efeitos sejam executados. Se seu useEffect verifica o que está focado, ele pode ver um estado obsoleto. Use um callback de ref ou requestAnimationFrame dentro de useEffect para verificar o foco após a pintura.
Consulte todos os elementos focáveis dentro do modal, então, ao pressionar a tecla Tab, redirecione o foco do último elemento de volta para o primeiro (e vice-versa com Shift+Tab). Use querySelectorAll com o seletor 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'.
:focus-visible quando você só precisa de estilos visuais (anéis de foco) para usuários do teclado, não cliques do mouseonFocus/onBlur do React quando você precisa executar lógica JavaScript ou rastrear o estado de foco no estado do React:focus-within quando você só precisa de mudanças de estilo do pai no foco do filhoComo onFocus do React borbulha, ele dispara toda vez que qualquer filho focável ganha foco. Use e.currentTarget.contains(e.relatedTarget as Node) para distinguir "foco entrou no contêiner" de "foco se moveu entre filhos".
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
e.currentTarget; // HTMLInputElement
// relatedTarget é tipado como EventTarget | null
const next = e.relatedTarget as HTMLElement | null;
if (next?.tagName === "BUTTON") { /* ... */ }
};const onFocus: React.FocusEventHandler<HTMLSelectElement> = (e) => {
// e é React.FocusEvent<HTMLSelectElement>
e.currentTarget; // HTMLSelectElement
};Revisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥