Hover e focus no Tailwind: estados, group e peer
hover:, focus-visible:, disabled:, group-hover: e peer-checked: — os estados que fazem um botão e um formulário parecerem prontos, com o CSS de cada um.
Uma variante Tailwind é uma condição colocada antes da utilidade. hover:bg-*
pinta durante hover; focus-visible:outline-* mostra foco quando a interação
pede um indicador; disabled:opacity-* reage ao atributo nativo. O CSS continua
baseado em pseudo-classes e seletores comuns.
O formulário da clínica será testado em repouso, teclado, erro e desabilitado. A aparência não pode ser a única fonte de verdade: atributos e texto mantêm o estado compreensível.
Hover vira uma condição de mídia e pseudo-classe
O botão começa com uma cor e muda apenas em dispositivos capazes de hover:
<button class="bg-cyan-600 text-white hover:bg-cyan-700">Salvar</button>O CLI 4.3.3 emitiu:
Hover é complemento. Um controle não pode revelar seu rótulo ou única ação somente quando o ponteiro passa, porque toque e teclado não oferecem o mesmo sinal.
focus-visible preserva a orientação pelo teclado
O anel deve contrastar com o botão e com o fundo ao redor. Não remova outline sem substituição.
<button class="rounded-lg bg-cyan-600 px-4 py-2 text-white
focus-visible:outline-2 focus-visible:outline-offset-2
focus-visible:outline-cyan-700">
Confirmar consulta
</button>focus: reage a todo foco. focus-visible: deixa o navegador considerar o
método de entrada. Em campo de texto, o foco costuma continuar visível; em botão
clicado pelo mouse, pode não aparecer. Teste com Tab, não apenas com clique.
Disabled precisa existir no HTML
Uma classe não desabilita comportamento. O atributo é quem impede a interação; a variante só apresenta esse estado.
<button disabled class="disabled:cursor-not-allowed disabled:opacity-50">
Enviando consulta
</button>Não use apenas opacidade em um link para fingir desabilitado. Link não possui
disabled nativo. Remova o destino ou use um botão quando a ação realmente é
controlada pela aplicação.
Atributos ARIA e data expõem estado sem classe dinâmica
O componente pode declarar a verdade em atributo e deixar a variante reagir:
<button aria-expanded="true"
class="aria-expanded:bg-cyan-700 aria-expanded:text-white">
Filtros
</button>Esse padrão evita manter duas verdades, como aberto no JavaScript e
classe-azul separada. ARIA não cria comportamento; o botão ainda precisa
alterar aria-expanded e controlar a região correspondente.
Valores de negócio podem usar data-*:
<span data-status="atrasada"
class="data-[status=atrasada]:bg-amber-100 data-[status=atrasada]:text-amber-900">
Atrasada
</span>group deixa um filho reagir ao pai
Marque o pai com group; o ícone usa group-hover. O seletor gerado procura um
ancestral ou grupo em hover.
<a class="group flex items-center justify-between" href="#agenda">
Abrir agenda
<span class="transition group-hover:translate-x-1">→</span>
</a>No CSS medido, a translação usa :is(:where(.group):hover *). A classe do filho
fica completa no arquivo, então a detecção é estática. Não use group para
esconder conteúdo essencial; ele é bom para reforço visual coordenado.
peer conecta irmãos pelo estado do campo
Um checkbox visual pode reagir ao input anterior. O input precisa vir antes porque o seletor usa irmãos seguintes.
<label class="flex items-center gap-2">
<input class="peer sr-only" type="checkbox">
<span class="size-5 rounded border peer-checked:bg-cyan-600"></span>
Receber lembrete
</label>sr-only mantém o campo para tecnologias assistivas. O label inteiro amplia o
alvo de clique. Se você usa hidden, o controle some também da árvore de
acessibilidade e do teclado.
Formulário precisa de mensagem, não só borda vermelha
Combine aria-invalid, descrição e variante. O texto explica o erro para quem
não percebe cor.
<label class="grid gap-1">
<span>E-mail</span>
<input aria-invalid="true" aria-describedby="erro-email"
class="border aria-invalid:border-rose-600 focus-visible:outline-2">
<span id="erro-email" class="text-sm text-rose-700">Informe um e-mail válido.</span>
</label>O plugin forms oferece base consistente para campos, mas não cria rótulo, validação nem mensagem. No teste com um único formulário, bundle completo teve 10.933 bytes crus e 2.862 em gzip. Avalie esse custo no contexto do projeto.
Transição só nas propriedades necessárias
Uma transição curta ajuda a perceber mudança; animação longa atrasa a tarefa.
<button class="transition-colors duration-150 hover:bg-cyan-700">Salvar</button>Evite transition-all: tamanho, posição e outras propriedades podem passar a
animar sem intenção. Respeite redução de movimento em deslocamentos e não dependa
da animação para comunicar conclusão.
Ordem das variantes é uma frase
md:hover:bg-cyan-700 significa: a partir de md, quando houver hover, mude o
fundo. dark:focus-visible:outline-white significa: dentro do modo escuro,
quando o foco visível ocorrer, use contorno branco. Leia da esquerda para a
direita e confira o seletor compilado quando a combinação parece opaca.
O modo escuro no Tailwind adiciona outra condição; responsividade explica intervalos de largura. Empilhar variantes funciona, mas uma frase com cinco condições pode indicar componente complexo demais.
Erro reproduzido: variante de foco digitada errado
Uma letra extra transforma um estado conhecido numa variante inexistente. O caso mínimo foi executado com Tailwind 4.3.3:
@import "tailwindcss";
.qa { @apply focus-visiblee:ring-2; }printf '@import "tailwindcss"; .qa { @apply focus-visiblee:ring-2; }\n' | npx @tailwindcss/cli@4.3.3 -i - -o /dev/nullO código de saída foi 1. Corrija para focus-visible:ring-2 e mantenha o teste
por teclado; trocar por focus: altera o contrato de interação.
Missão de quatro estados
Monte um campo com rótulo e mensagem, mais um botão que possua repouso, hover, foco por teclado e disabled. Navegue apenas com Tab e Shift+Tab. A missão termina quando o foco nunca desaparece, o erro continua claro sem cor e o botão desabilitado usa atributo real. Depois transforme o padrão em componente React, sem montar nomes de classe em pedaços.
Monte uma tabela de estados antes de extrair o componente
Liste repouso, hover, foco, pressed, loading, disabled, sucesso e erro. Nem todo controle precisa de todos, mas cada estado aceito precisa ter uma fonte de verdade. Hover vem do apontador; foco vem do navegador; disabled vem de atributo; loading normalmente vem da aplicação e precisa de texto acessível.
Para cada linha, anote mudança visual, atributo e mensagem. Se erro só troca a borda para vermelho, falta descrição. Se loading reduz opacidade mas permite segundo envio, falta comportamento. Essa tabela evita que a string de classes pareça completa quando o contrato ainda está vazio.
Teste combinações: disabled com dark, erro com foco, loading numa tela estreita. Variantes empilhadas podem gerar seletores corretos e ainda produzir contraste ruim. Use teclado, toque e ponteiro; nenhuma interação representa as outras.
Ao extrair o componente, limite variantes de negócio a strings completas. O mapa torna a detecção estática e a revisão explícita. O guia completo de Tailwind mostra onde esse inventário de estados entra na passagem de uma tela para um sistema reutilizável.
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em @tailwindcss/cli 4.3.3 com @tailwindcss/forms 0.5.11 no Node 26.3.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- Tailwind CSS — Hover, focus, and other states — tailwindcss.com
- MDN — :focus-visible — developer.mozilla.org
- WCAG 2.2 — Focus appearance — w3.org


