Na era do jQuery, o fade ao passar o rato resolvia-se com .animate() ou .fadeIn(). Em 2026, isso é desnecessário para estados simples de interface: o navegador interpola opacity diretamente com CSS. O resultado é menos script, menos risco de layout thrashing e um efeito que respeita as preferências do sistema quando se liga prefers-reduced-motion.
Este artigo é uma receita prática de front-end para temas WordPress e Gutenberg. Começa com um fade mínimo ao passar o rato e passa depois por transition versus animation, :focus-visible para teclado, movimento reduzido, wp_enqueue_style correto, armadilhas típicas do CSS de blocos e a razão pela qual as propriedades tratadas pelo compositor mantêm os 60 fps de forma mais previsível do que width ou top.
Transições CSS ou JavaScript para efeitos hover
JavaScript é a escolha certa quando o estado depende da rede, da validação de formulários ou de sequências complexas. Para “o rato passa sobre a imagem, escurece um pouco”, o CSS é mais fácil de ler e mais barato de executar.
Vantagens concretas na prática:
- Não há event listeners para limpar em ecrãs de administração com comportamento de SPA.
- O navegador pode tratar
opacityetransformna thread do compositor quando a camada já existe. - O mesmo conjunto de regras serve o rato e o foco quando o estado é espelhado com
:focus-visible. - Evitam-se conflitos entre
mouseenter/mouseleavee um CSS que já está a animar.
A exceção é quando o efeito tem de sincronizar com uma camada controlada por script (por exemplo, uma lightbox própria que define aria-hidden). Mesmo aí, o fade pode ficar no CSS e o JavaScript limita-se a trocar uma classe.
Como criar um efeito fade básico com CSS
Imagine uma imagem que deve escurecer quando o rato passa por cima. Defina o valor inicial de forma explícita, declare que propriedade deve ser interpolada e altere o valor em :hover.
.hover-image {
opacity: 1;
transition: opacity 0.3s ease-in-out;
}
.hover-image:hover {
opacity: 0.7;
}O que acontece:
- A imagem começa com
opacity: 1(totalmente visível). - Ao passar o rato, o valor vai até
0.7em 300 ms. - A função
ease-in-outsuaviza o início e o fim. - Quando o hover termina, a transição regressa a
1com a mesma regra.
opacity é um número entre 0 e 1 e afeta o elemento inteiro, incluindo os filhos. Se só o fundo deve escurecer, background-color com rgba() ou color-mix() é muitas vezes mais preciso do que escurecer o cartão todo.
Diferença entre transition e animation em CSS
transition e animation não são ferramentas intermutáveis. A escolha depende do que desencadeia o movimento.
Use transition quando:
- Uma propriedade muda entre dois estados (hover, foco, classe aberta ou fechada).
- O navegador deve interpolar a partir do valor atual até um valor novo.
- O efeito deve inverter a meio caminho se o utilizador afastar o rato.
Use @keyframes / animation quando:
- O movimento deve correr sem interação do utilizador (carregamento, pulsação discreta, introdução).
- São precisos vários keyframes, atrasos por etapa ou
animation-iteration-count. - O estado não é uma simples mudança de A para B.
/* Transition: guiada por mudança de estado */
.btn {
opacity: 1;
transition: opacity 0.2s ease-out;
}
.btn:hover {
opacity: 0.85;
}
/* Animation: sequência temporizada */
@keyframes fade-in-once {
from { opacity: 0; }
to { opacity: 1; }
}
.hero-title {
animation: fade-in-once 0.4s ease-out both;
}Um erro comum em temas é “animar” o hover com uma animation infinita e depois lutar contra animation-fill-mode. Para hover, transition é quase sempre a solução mais limpa.
Efeito hover com focus-visible para teclado
:hover sozinho não chega para quem navega com teclado. Espelhe o feedback visual com :focus-visible, para que a navegação com Tab tenha o mesmo fade sem que cada clique do rato deixe um anel de foco visível.
.card-media {
opacity: 1;
outline: none;
transition: opacity 0.25s ease, box-shadow 0.25s ease;
}
.card-media:hover,
.card-media:focus-visible {
opacity: 0.75;
}
.card-media:focus-visible {
box-shadow: 0 0 0 3px color-mix(in srgb, CanvasText 40%, transparent);
}Notas práticas:
- Use
:focus-visible, e não apenas:focus, para evitar a marcação de foco depois de um clique do rato nos navegadores modernos. - Garanta que o elemento pode mesmo receber foco (
<a>,<button>outabindex="0"com tratamento de teclado). - Não remova
outlinesem o substituir. O fade sozinho não dá contraste suficiente como indicador de foco.
Em ecrãs táteis, :hover fica “preso” em alguns navegadores. Mantenha o fade discreto (0.7 a 0.9) para que um hover bloqueado não pareça um erro.
Como desativar a animação hover com prefers-reduced-motion
Alguns utilizadores pediram ao sistema menos movimento. Respeite essa escolha com uma media query que desliga transições e animações.
@media (prefers-reduced-motion: reduce) {
.hover-image,
.card-media,
.btn {
transition: none;
animation: none;
}
}Também é possível encurtar a duração em vez de anular tudo, mas para um fade em hover none funciona: o estado continua a mudar, só sem interpolação.
Teste no macOS (definição de redução de movimento nas opções de acessibilidade) e no Windows 11 (efeitos de animação nas opções de acessibilidade e efeitos visuais). Confirme que o indicador de foco continua visível com a transição desligada.
Como compreender as propriedades de transição CSS
Transition-property
Indique sempre propriedades concretas. transition: all é tentador, mas anima também coisas em que não pensou (por exemplo border-width ou font-size vindos de outra regra) e custa mais.
/* Preferir */
transition-property: opacity, transform;
/* Evitar em componentes de interface */
transition-property: all;Transition-duration
Em interfaces, 150 a 300 ms é normalmente a zona em que o efeito se nota sem parecer lento. Abaixo de 100 ms pode parecer um corte seco. Acima de 400 ms, o hover começa a parecer lento no desktop.
/* Muito rápido (subtil) */
transition-duration: 0.15s;
/* Padrão para interface */
transition-duration: 0.3s;Transition-timing-function
Controla a curva de aceleração. É isto que faz as animações parecerem naturais.
ease-out: arranque rápido, aterragem suave, muitas vezes o melhor à saída do hover.ease-in-out: suavidade simétrica, adequada a um fade de opacity.linear: raramente desejável em interfaces, mais útil em indicadores de progresso.cubic-bezier(...): quando a marca tem uma curva de movimento própria.
Transition-delay
O atraso é útil quando uma camada interior deve esperar por uma exterior. Num fade simples de imagem, o atraso é normalmente 0s; caso contrário, a interface parece lenta.
Qual é a sintaxe abreviada completa de transition
A forma abreviada é property duration timing-function delay:
.hover-image {
transition: opacity 0.3s ease-in-out 0s;
}
/* Várias propriedades */
.card-img {
transition:
opacity 0.25s ease-out,
transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}Mantenha a ordem consistente em todo o tema. Quando um componente usa a forma longa e outro a abreviada com valores diferentes, a linguagem de movimento fica desarrumada.
Como fazer efeitos hover avançados em CSS sem jQuery
1. Fade + escala (efeito zoom)
Combine opacidade e transformação para um efeito de zoom. transform: scale() junta-se bem a opacity porque ambas podem ser aceleradas pelo compositor.
.card-img {
opacity: 1;
transform: scale(1);
transition:
opacity 0.3s ease-in-out,
transform 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.card-img:hover,
.card-img:focus-visible {
opacity: 0.9;
transform: scale(1.04);
}Evite animar width / height para o zoom. Isso obriga a layout e paint em cada frame.
2. Fade numa imagem dentro de um contentor
Perfeito para galerias de imagens. Quando a área de hover é maior do que a imagem (o cartão inteiro), coloque :hover no contentor:
.image-container img {
opacity: 1;
transition: opacity 0.3s ease-in-out;
}
.image-container:hover img,
.image-container:focus-within img {
opacity: 0.7;
}:focus-within cobre o foco de teclado no link dentro do cartão.
Como carregar a folha de estilos do fade no WordPress
Não coloque o CSS do fade num bloco <style> solto em header.php se o puder evitar. Use a API de enqueue do tema, para que dependências, versões e carregamento sejam previsíveis.
<?php
declare(strict_types=1);
add_action('wp_enqueue_scripts', static function (): void {
$theme_version = wp_get_theme()->get('Version');
$css_path = get_theme_file_path('assets/css/hover-fade.css');
$css_ver = file_exists($css_path) ? (string) filemtime($css_path) : $theme_version;
wp_enqueue_style(
'theme-hover-fade',
get_theme_file_uri('assets/css/hover-fade.css'),
[],
$css_ver
);
});Porquê este padrão:
filemtimeinvalida a cache quando o ficheiro muda, sem subir a versão à mão.- O handle
theme-hover-fadepode ser removido ou substituído num tema filho. - Evitam-se duplicados se um bloco ou plugin também tentar injetar as mesmas regras.
Em temas de blocos (FSE) continua a ser possível carregar uma pequena folha utilitária, ou colocar classes utilitárias em theme.json quando faz sentido. Regras de hover maiores, com media queries, continuam a pertencer a ficheiros CSS.
Se o efeito só é necessário num modelo, condicione o enqueue com is_singular() / is_page_template() para manter o CSS crítico mais pequeno.
Porque é que o fade não funciona em blocos Gutenberg
O Gutenberg altera a estrutura do DOM mais do que os modelos clássicos. Um fade que “funcionava no Classic” falha muitas vezes por estas razões:
Seletor errado no wrapper
Os blocos são muitas vezes envolvidos em .wp-block-image, .wp-block-group ou figure/figcaption. Uma regra que aponta diretamente para img:hover não funciona quando o hover acontece no link que envolve a imagem.
/* Demasiado estreito em muitos layouts de blocos */
.wp-block-image img:hover {
opacity: 0.7;
}
/* Mais robusto */
.wp-block-image a:hover img,
.wp-block-image a:focus-visible img,
.wp-block-image figure:hover img {
opacity: 0.7;
}Estilos inline e imagens de conteúdo
Estilos de imagem definidos no editor (style="width:...") não competem necessariamente com opacity, mas um !important num reset do tema ou no CSS de um plugin pode competir. Mantenha a especificidade baixa e consistente; evite !important no fade.
CSS adicional versus folha de estilos do bloco
O “CSS adicional” em Apresentação e Personalizar serve para protótipos. Em produção, use um ficheiro próprio via enqueue, ou um estilo específico do bloco registado com wp_enqueue_block_style, para que as regras só carreguem onde o bloco é usado.
Interatividade e blocos aninhados
Blocos de grupo com links no interior podem ativar o hover no pai enquanto o foco está no filho. Teste com rato e com Tab. :focus-within no wrapper do cartão resolve muitos destes casos.
Editor versus frontend
O canvas do editor nem sempre carrega o CSS de frontend do tema. Se o cliente “não vê o fade no editor”, isso é esperado, a não ser que também carregue os estilos via enqueue_block_assets ou add_editor_style. Documente a diferença: o efeito aparece no site publicado, não necessariamente no canvas.
Que propriedades CSS são rápidas de animar
Animar propriedades que obrigam a layout (width, height, top, left, margin) ou a paint dispendioso (box-shadow em superfícies enormes sem camada própria) arrisca engasgos no telemóvel.
Prefira para fade e microinterações:
opacitytransform(translate, scale, rotate)
Cuidado com:
filter: blur()em imagens grandes (pode ser dispendioso)- valores de
box-shadowanimados em secções inteiras do ecrã transition: all
Raramente é preciso will-change: opacity em cada imagem de uma grelha. will-change reserva memória; use-o temporariamente em elementos que animam com frequência, ou confie que o navegador promove a camada durante a transição.
Meça com o painel Performance: procure frames longos durante o hover numa lista com muitos cartões. Se aparecer layout thrashing, quase sempre é a propriedade errada que está a ser animada.
Como resolver um fade que não aparece
- Confirme que o valor inicial e o final são mesmo diferentes.
- Confirme que
transitionestá na regra base, não só em:hover. A transição é lida do estilo para o qual o elemento muda, por isso sem ela falta a transição de regresso. - Procure outras regras que definam
transition: noneglobalmente. - Verifique
prefers-reduced-motionno sistema operativo. - No WordPress, confirme que a folha de estilos está carregada (código-fonte ou separador Network) e que o seletor corresponde ao DOM do bloco renderizado.
Que regra de transition se aplica quando o hover começa e termina
O CSS Transitions Module Level 1 continua a ser um W3C Working Draft, publicado pela última vez a 8 de janeiro de 2026, e não uma recomendação final. A especificação define que a duração, o atraso e a função de temporização vêm do after-change style do elemento, ou seja, do estilo para o qual o elemento está a mudar. Quando o ponteiro entra na imagem, aplica-se o transition da regra :hover. Quando o ponteiro sai, aplica-se o transition da regra base.
Daqui saem duas consequências práticas:
- Se
transitionestiver só em:hover, a imagem escurece com fade, mas volta de repente quando o ponteiro sai, porque a regra base não tem transição. - É possível dar velocidades diferentes à entrada e à saída de propósito, por exemplo uma resposta rápida à entrada e um regresso mais calmo.
.hover-image {
opacity: 1;
transition: opacity 0.3s ease-out; /* usado no regresso */
}
.hover-image:hover {
opacity: 0.7;
transition-duration: 0.15s; /* usado à entrada */
}A especificação cobre também o caso em que o ponteiro sai antes de o fade terminar. A secção 3.1, sobre a inversão mais rápida de transições interrompidas, explica que um regresso com a duração completa sobre uma distância menor parece assimétrico. Por isso, a transição de regresso é encurtada por um fator próprio (reversing shortening factor), e a função de temporização continua a ser usada por inteiro, sem saltar para o meio da curva. Não é preciso JavaScript para calcular o tempo que falta. Fonte: CSS Transitions Module Level 1.
Um fade de opacity conta como movimento segundo a WCAG
O critério de sucesso 2.3.3 Animation from Interactions da WCAG 2.2 (nível AAA) exige que a animação de movimento desencadeada por interação possa ser desativada, a menos que seja essencial para a funcionalidade ou para a informação. O documento explicativo do W3C para o 2.3.3 delimita o conceito: alterações de cor ou de opacity que não mudam o tamanho, a forma ou a posição percebidos não contam como animação de movimento. Uma errata retirou o desfoque desta exceção, por isso filter: blur() já não está incluído.
Um fade só de opacity fica assim fora do 2.3.3, enquanto transform: scale() muda o tamanho percebido e fica dentro. Isto permite uma regra de movimento reduzido mais precisa do que desligar tudo:
@media (prefers-reduced-motion: reduce) {
.card-img {
transform: none;
transition: opacity 0.2s ease-out;
}
}O MDN descreve a mesma abordagem na documentação de prefers-reduced-motion: uma animação de escala é atenuada para uma animação de dissolução, que não desencadeia distúrbios vestibulares. O valor reduce é avaliado como verdadeiro, por isso @media (prefers-reduced-motion) equivale a @media (prefers-reduced-motion: reduce).
Para o foco aplica-se outro critério. O 2.4.7 Focus Visible (nível AA) exige um indicador de foco visível, e o MDN remete ainda para o 1.4.11 Non-text Contrast, que pede um contraste mínimo de 3:1 para o indicador. Uma mudança de opacity numa imagem raramente cumpre isso sozinha, por isso o foco precisa de um outline ou box-shadow próprio.
Porque é que opacity 0 não esconde um elemento
A documentação do MDN sobre opacity aponta três pontos que muitas vezes escapam nos efeitos hover:
- Um elemento com
opacity: 0fica invisível, mas continua no DOM. Recebe eventos do ponteiro e pode receber foco se estiver na ordem de tabulação. Um botão de overlay escondido pode, portanto, ser clicado e focado. Torne-o visível quando recebe foco, ou desligue os eventos do ponteiro compointer-eventse retire-o da ordem de tabulação. - A opacity, por si só, não esconde conteúdo dos leitores de ecrã. Para isso usa-se o atributo
hidden,visibilityoudisplay. - Qualquer valor abaixo de
1cria um novo stacking context. Um overlay comz-indexdentro de um cartão que passa para0.9pode acabar numa ordem de empilhamento diferente da esperada.
Se só o fundo deve escurecer, o MDN recomenda uma cor de fundo com canal alfa em vez de opacity no elemento inteiro. Com texto sobre a imagem, o contraste entre texto e fundo tem de continuar suficiente depois do fade.
Fade de entrada na primeira apresentação com @starting-style
Uma transition só arranca quando um valor muda. O MDN explica que, por omissão, as transições não são desencadeadas na primeira atualização de estilo do elemento, nem quando display passa de none para um valor visível. Um cartão inserido no DOM com opacity: 1 não aparece, portanto, com fade por si só.
@starting-style define os valores a partir dos quais a transição começa. Para elementos que alternam com display: none, é ainda preciso transition-behavior: allow-discrete, porque display tem um tipo de animação discreto. Na transição para none, o valor só muda a 100 por cento da duração, por isso o elemento fica visível durante todo o fade.
.notice {
opacity: 1;
transition: opacity 0.3s ease-out;
transition: opacity 0.3s ease-out, display 0.3s allow-discrete;
}
.notice.is-hidden {
display: none;
opacity: 0;
}
@starting-style {
.notice {
opacity: 0;
}
}A primeira linha de transition é intencional: o MDN escreve a propriedade duas vezes para que os navegadores sem suporte para allow-discrete continuem a animar a opacity. @starting-style tem a mesma especificidade que a regra original e por isso tem de vir depois dela. O MDN marca ambas as funcionalidades como Baseline 2024. Navegadores mais antigos mostram o elemento sem fade, o que é uma alternativa aceitável para uma transição puramente visual.
Como reagir ao fim do fade com transitionend
Quando o JavaScript só troca uma classe e o CSS faz o fade, o script pode escutar transitionend para saber quando a transição terminou, por exemplo antes de definir hidden ou de mover o foco. A página do MDN sobre transitionend e a especificação dão quatro regras sobre quando o evento chega de facto:
transitionenddispara nos dois sentidos: quando o elemento atinge o estado de hover e quando regressa por completo.- Se a transição for removida antes de terminar, por exemplo porque
transition-propertyé removida oudisplaypassa anone, não hátransitionend. - Se disparar
transitioncancel, por exemplo porque o ponteiro sai a meio do fade, não hátransitionendpara essa transição. - Se a duração e o atraso forem ambos
0s, não existe transição e nenhum dos eventos de transição dispara.
A última regra é a que mais falha na prática. Quando prefers-reduced-motion define transition: none, um script que espera por transitionend para arrumar o estado fica à espera para sempre. Filtre por propertyName e preveja um caminho alternativo quando não há transição a correr.
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
// No CSS: .is-fading-out { opacity: 0; }
function hideNotice(el) {
el.classList.add('is-fading-out');
if (reduce) {
el.hidden = true;
return;
}
el.addEventListener('transitionend', function onEnd(event) {
if (event.propertyName !== 'opacity') return;
el.removeEventListener('transitionend', onEnd);
el.hidden = true;
});
}Hover em ecrãs táteis com a media query hover
A funcionalidade de media hover, do Media Queries Level 4, testa se o mecanismo de entrada principal consegue pairar sobre elementos. Segundo o MDN, devolve none quando a entrada principal não consegue pairar de todo ou não o faz de forma prática, como muitos telemóveis que emulam o hover com um toque longo. Coloque a regra de hover dentro de @media (hover: hover) quando um hover preso no telemóvel é um problema, e deixe :focus-visible fora, para que o foco de teclado continue a dar o mesmo estado.
@media (hover: hover) {
.card-media:hover {
opacity: 0.75;
}
}
.card-media:focus-visible {
opacity: 0.75;
}Suporte dos navegadores para transition, focus-visible e prefers-reduced-motion
A tabela mostra a primeira versão com suporte sem prefixo, segundo o MDN browser-compat-data na versão 8.1.6 (outubro de 2026).
| Funcionalidade | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
transition | 26 | 16 | 9 | 12 |
:focus-within | 60 | 52 | 10.1 | 79 |
:focus-visible | 86 | 85 | 15.4 | 86 |
prefers-reduced-motion | 74 | 63 | 10.1 | 79 |
@media (hover) | 38 | 64 | 9 | 12 |
transition-behavior | 117 | 129 | 17.4 | 117 |
@starting-style | 117 | 129 | 17.5 | 117 |
O Internet Explorer suporta transition a partir da versão 10, mas nenhuma das outras funcionalidades da tabela. O Safari tinha transition com o prefixo -webkit- desde a versão 3.1.
Há uma consequência desta tabela fácil de ignorar. Num navegador sem :focus-visible, como o Safari anterior à 15.4, a regra .card-media:hover, .card-media:focus-visible é ignorada por inteiro, incluindo a parte do hover. O MDN explica no artigo sobre listas de seletores que um único seletor desconhecido invalida a lista toda. Escreva hover e foco como duas regras separadas, ou envolva-os em :is(), que aceita uma lista de seletores tolerante.
O que lembrar sobre fade e hover em CSS
As transições CSS são a forma moderna e fácil de manter de fazer fade em hover em temas WordPress.
Pontos-chave:
- Anime
opacity(e, se fizer sentido,transform), não propriedades de layout. - Use
transitionpara estados eanimationpara sequências temporizadas. - Espelhe o hover com
:focus-visible/:focus-within. - Respeite
prefers-reduced-motion. - Carregue o CSS via
wp_enqueue_stylee ajuste os seletores aos wrappers do Gutenberg. - Mantenha a duração em cerca de 0,2 a 0,3 s nas interfaces.
- Use
@starting-stylequando um elemento deve aparecer com fade na primeira apresentação.
Transições em transform e opacity são baratas porque não forçam reflow, e essa diferença nota-se logo num portátil antigo ou num telemóvel de gama média. Aplicamos estes detalhes de front-end em cada tema feito à medida em WordPress.







