Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

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.

Rodolfo Mori5 min de leitura

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:

html
<button class="bg-cyan-600 text-white hover:bg-cyan-700">Salvar</button>

O CLI 4.3.3 emitiu:

@media (hover: hover) { .hover\:bg-cyan-700:hover { background-color: var(--color-cyan-700); } }

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.

html
<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-visible\:outline-2:focus-visible { outline-width: 2px; } .focus-visible\:outline-offset-2:focus-visible { outline-offset: 2px; }

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.

html
<button disabled class="disabled:cursor-not-allowed disabled:opacity-50">
  Enviando consulta
</button>
.disabled\:opacity-50:disabled { opacity: 50%; }

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:

html
<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-*:

html
<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.

html
<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.

html
<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>
.peer-checked\:bg-cyan-600:is(:where(.peer):checked ~ *) { background-color: var(--color-cyan-600); }

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.

html
<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.

html
<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:

css
@import "tailwindcss";
.qa { @apply focus-visiblee:ring-2; }
bash
printf '@import "tailwindcss"; .qa { @apply focus-visiblee:ring-2; }\n' | npx @tailwindcss/cli@4.3.3 -i - -o /dev/null
Error: Cannot apply utility class `focus-visiblee:ring-2` because the `focus-visiblee` variant does not exist.

O 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.

  • tailwind
  • hover
  • focus
  • group
  • peer
  • formulários
  • acessibilidade

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. Tailwind CSS — Hover, focus, and other states — tailwindcss.com
  2. MDN — :focus-visible — developer.mozilla.org
  3. WCAG 2.2 — Focus appearance — w3.org

Continue por aqui