CSS oferece duas ferramentas para movimento: transições e animações @keyframes. A maioria dos desenvolvedores sabe que ambas existem, mas recorre àquela que aprendeu primeiro — geralmente transições — e depois fica se perguntando por que o spinner de carregamento não faz loop ou por que a notificação volta de repente à posição original. A decisão não é complicada quando você entende para que cada uma serve. Transições são uma reação: algo muda de estado, o navegador suaviza a mudança. Animações @keyframes são scripts independentes: rodam por conta própria, fazem loop se você quiser e não precisam de um gatilho de estado. Este guia cobre os dois, as propriedades que mais importam, e os detalhes de desempenho que separam animações suaves de 60fps das que travam. Se você escreve muito CSS, vale marcar o CSS Formatter nos favoritos — ele limpa qualquer shorthand bagunçado rapidamente.
Transições CSS — O Caso Simples
Uma transição diz ao navegador: "quando esta propriedade mudar, anime a mudança ao longo do tempo em vez de pular." Só isso. O uso mais comum é em efeitos de hover — mudanças de cor, fades de opacidade, transformações sutis de escala. O guia de transições do MDN cobre o spec completo, mas as quatro propriedades que importam são transition-property, transition-duration, transition-timing-function e transition-delay.
/* O shorthand: property duration easing delay */
.btn-primary {
background-color: #4f46e5;
color: #fff;
padding: 0.625rem 1.25rem;
border-radius: 6px;
border: none;
cursor: pointer;
transition: background-color 200ms ease, transform 150ms ease;
}
.btn-primary:hover {
background-color: #4338ca;
transform: translateY(-1px);
}
.btn-primary:active {
transform: translateY(0);
}
/* Fade de opacidade — um card entrando na tela */
.notification-card {
opacity: 0;
transition: opacity 300ms ease-in-out;
}
.notification-card.is-visible {
opacity: 1;
}Você pode listar várias transições separadas por vírgulas, cada uma com sua própria duração e easing — é exatamente o que o exemplo do botão acima faz. Cada propriedade é animada independentemente.
transition: all. É tentador como um pega-tudo, mas significa que toda mudança de propriedade naquele elemento é animada — incluindo as que você não pretendia. Também faz o navegador trabalhar mais em cada frame. Liste as propriedades específicas que você quer transicionar. O docs do MDN sobre transition-property explica quais propriedades são animáveis.transition-timing-function — Easing Explicado
A função de timing controla a curva de aceleração da animação — se começa rápido e desacelera, move em velocidade constante ou salta por uma curva personalizada. As palavras-chave embutidas cobrem a maioria das situações.
ease— começa devagar, acelera e depois desacelera perto do fim. O padrão. Bom para a maioria dos movimentos de UI.linear— velocidade constante do início ao fim. Bom para spinners e barras de progresso, ruim para a maioria das outras coisas — parece mecânico.ease-in— começa devagar, termina rápido. Parece algo ganhando velocidade. Bom para elementos saindo da tela.ease-out— começa rápido, termina devagar. Parece natural para elementos entrando na tela (como uma notificação deslizando para dentro).ease-in-out— devagar nas duas extremidades. Parece polido para elementos que começam e param na tela.cubic-bezier(x1, y1, x2, y2)— defina sua própria curva. Ferramentas como cubic-bezier.com permitem construir e visualizar curvas personalizadas.
/* Easing personalizado com efeito de mola usando cubic-bezier */
.drawer {
transform: translateX(-100%);
transition: transform 350ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.drawer.is-open {
transform: translateX(0);
}
/* Os valores de cubic-bezier acima ultrapassam ligeiramente (y > 1)
o que dá um satisfatório efeito de mola ao abrir */@keyframes — Quando Transições Não São Suficientes
Transições têm um limite claro: precisam de dois estados e um gatilho. Se você quer que algo faça loop indefinidamente, rode no carregamento da página sem nenhuma interação, ou anime por mais de dois passos, você precisa de @keyframes. A referência @keyframes do MDN cobre a sintaxe completa. Você define os keyframes separadamente e depois os anexa a um elemento com a propriedade animation.
/* Definir a animação */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Anexá-la a um elemento */
.spinner {
width: 24px;
height: 24px;
border: 3px solid #e5e7eb;
border-top-color: #4f46e5;
border-radius: 50%;
animation: spin 700ms linear infinite;
}
/* Multi-step — mais de dois pontos de parada */
@keyframes pulse-ring {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.15); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
.badge-live {
animation: pulse-ring 1.5s ease-in-out infinite;
}O Shorthand animation — Todas as Oito Propriedades
O shorthand animation empacota oito propriedades em uma única declaração. Você não precisa de todas toda vez, mas saber o que cada uma controla te salva de vasculhar a documentação quando algo não se comporta como esperado.
/*
animation: name | duration | timing-function | delay | iteration-count | direction | fill-mode | play-state
*/
.toast {
animation: slide-in-right 350ms ease-out 0s 1 normal forwards running;
}
/* Mais comumente escrito com apenas os valores necessários: */
.toast {
animation: slide-in-right 350ms ease-out forwards;
}
/* Dividido em propriedades individuais para clareza: */
.confetti-piece {
animation-name: float-down;
animation-duration: 2s;
animation-timing-function: ease-in;
animation-delay: calc(var(--i) * 150ms); /* escalonado via propriedade personalizada CSS */
animation-iteration-count: 1;
animation-direction: normal;
animation-fill-mode: forwards;
animation-play-state: running;
}
/* Pausando uma animação via JavaScript: alternar uma classe */
.spinner.is-paused {
animation-play-state: paused;
}animation-fill-mode — Corrigindo o Problema do Snap-Back
Esta é a propriedade que tropeça todo mundo em algum momento. Por padrão, quando uma animação termina, o elemento volta repentinamente para seus estilos pré-animação — como se a animação nunca tivesse acontecido. Se você anima uma notificação toast deslizando da direita, ela vai se mover de volta para fora da tela assim que a animação terminar. animation-fill-mode: forwards corrige isso mantendo o elemento no estado do seu keyframe final quando a animação termina.
@keyframes slide-in-right {
from {
transform: translateX(110%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Sem forwards: o toast desliza para dentro, depois VOLTA ABRUPTAMENTE para fora da tela */
.toast-bad {
animation: slide-in-right 350ms ease-out;
}
/* Com forwards: o toast desliza para dentro e FICA na posição */
.toast {
animation: slide-in-right 350ms ease-out forwards;
}
/*
valores de fill-mode:
- none (padrão) — sem estilos aplicados antes ou depois
- forwards — manter o keyframe final após o término da animação
- backwards — aplicar o primeiro keyframe durante o período de delay
- both — forwards + backwards combinados
*/Exemplos Reais — Skeleton Loader, Spinner, Toast, Badge Pulsante
Aqui estão quatro padrões que você usará constantemente. Cada um demonstra uma combinação específica de @keyframes e propriedades de animação.
/* --- 1. Shimmer de carregamento skeleton --- */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite linear;
border-radius: 4px;
}
.skeleton-title { height: 20px; width: 60%; margin-bottom: 12px; }
.skeleton-text { height: 14px; width: 100%; margin-bottom: 8px; }
.skeleton-text:last-child { width: 80%; }/* --- 2. Loader girando --- */
@keyframes spin {
to { transform: rotate(360deg); }
}
.loader-spinner {
display: inline-block;
width: 32px;
height: 32px;
border: 3px solid rgba(79, 70, 229, 0.2);
border-top-color: #4f46e5;
border-radius: 50%;
animation: spin 600ms linear infinite;
}/* --- 3. Toast de notificação deslizante --- */
@keyframes toast-enter {
from {
transform: translateX(calc(100% + 1.5rem));
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes toast-exit {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(calc(100% + 1.5rem));
opacity: 0;
}
}
.toast {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
padding: 0.875rem 1.25rem;
background: #1f2937;
color: #fff;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
animation: toast-enter 350ms ease-out forwards;
}
.toast.is-dismissing {
animation: toast-exit 300ms ease-in forwards;
}/* --- 4. Badge de notificação pulsante --- */
@keyframes badge-pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.5);
}
50% {
box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);
}
}
.notification-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 5px;
background: #ef4444;
color: #fff;
font-size: 11px;
font-weight: 700;
border-radius: 999px;
animation: badge-pulse 2s ease-in-out infinite;
}Desempenho — Propriedades Compostas pelo GPU vs Que Disparam Layout
Nem todas as propriedades CSS são iguais quando se trata de desempenho de animação. O pipeline de renderização do navegador tem três estágios que importam aqui: layout (calculando tamanhos e posições dos elementos), paint (preenchendo pixels) e composite (combinando camadas no GPU). Animar uma propriedade que dispara layout significa que o navegador recalcula toda a geometria do documento em cada frame — isso é caro e é o que causa travamentos. O guia de desempenho de animações do web.dev cobre isso em profundidade.
- Seguro para animar (composição GPU):
transform(translate, scale, rotate) eopacity. Estes rodam inteiramente na thread do compositor GPU — nunca disparam layout ou paint. - Causa paint (evitar se possível):
color,background-color,border-color,box-shadow. Estes pulam o layout mas disparam um repaint em cada frame. Transições curtas (abaixo de 300ms) geralmente estão bem. - Causa layout (nunca animar):
width,height,top,left,margin,padding. Cada frame dispara um recálculo completo de layout — travamento garantido em páginas complexas.
transform e opacity. Se quiser mover algo, use translate em vez de left/top. Se quiser redimensionar algo, use scale em vez de width/height. CSS Triggers é uma referência que lista exatamente quais estágios de renderização cada propriedade aciona./* Ruim — animar width dispara layout em cada frame */
.progress-bar-bad {
transition: width 300ms ease;
}
/* Bom — usar transform scaleX em vez disso */
.progress-bar {
transform-origin: left center;
transition: transform 300ms ease;
}
/* No JS, defina: progressBar.style.transform = 'scaleX(0.75)' para 75% */
/* Ruim — animar top/left (posiciona o elemento com layout) */
.tooltip-bad {
position: absolute;
top: 0;
transition: top 200ms ease;
}
/* Bom — usar translateY em vez disso */
.tooltip {
position: absolute;
top: 0;
transform: translateY(0);
transition: transform 200ms ease;
}will-change — Use com Moderação
will-change é uma dica ao navegador de que uma propriedade específica está prestes a ser animada, então ele deve promover o elemento para sua própria camada de compositor com antecedência. Isso pode eliminar o breve flash de travamento que às vezes você vê no início de uma animação em hardware de baixo desempenho. Mas tem um custo real: cada camada promovida consome memória GPU. Se você colocar will-change: transform em cada elemento animado do seu app, você vai degradar o desempenho na maioria dos dispositivos — o oposto do que queria.
/* Jeito certo — adicione logo antes da animação começar, remova depois */
.modal-overlay {
/* Não defina will-change aqui por padrão */
}
/* Adicione via JavaScript apenas quando o usuário acionar abertura do modal */
/* overlay.style.willChange = 'opacity'; */
/* overlay.addEventListener('transitionend', () => { */
/* overlay.style.willChange = 'auto'; */
/* }); */
/* Aceitável em CSS para elementos que animam frequentemente
(ex.: um botão de ação flutuante persistente no scroll) */
.fab {
will-change: transform; /* OK — este elemento genuinamente anima no scroll */
transition: transform 200ms ease;
}
/* Não faça isso — desperdiça memória GPU sem nenhum benefício */
.card {
will-change: transform; /* RUIM — o card só anima no hover, não constantemente */
}prefers-reduced-motion — Acessibilidade que Você Não Pode Pular
Uma parcela significativa dos usuários tem distúrbios vestibulares ou outras condições em que o movimento provoca desconforto ou náusea. A media query prefers-reduced-motion permite respeitar a configuração de "reduzir movimento" no nível do sistema. A diretriz WCAG 2.1 2.3.3 cobre esse requisito, e a referência prefers-reduced-motion do MDN mostra o suporte de navegadores (agora universal). O padrão é simples: envolva suas animações na media query padrão e forneça um fallback sem movimento dentro da de reduced-motion.
/* Defina animações normalmente para usuários que estão bem com movimento */
@keyframes slide-in-right {
from { transform: translateX(110%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.toast {
animation: slide-in-right 350ms ease-out forwards;
}
.spinner {
animation: spin 600ms linear infinite;
}
/* Override para usuários que preferem movimento reduzido */
@media (prefers-reduced-motion: reduce) {
.toast {
/* Sem deslizamento — apenas aparecer */
animation: none;
opacity: 1;
transform: none;
}
.spinner {
/* Desacelerar muito ou parar completamente */
animation-duration: 4s;
}
/* Opção nuclear — desativar TODAS as animações no site */
/* Use isso apenas se você não tiver auditado cada animação individualmente */
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}A "opção nuclear" no final desse snippet é um padrão comum para codebases existentes que não auditaram cada animação. É melhor do que nada, mas auditar cada animação individualmente dá mais controle — algumas animações transmitem estado (uma barra de progresso, um spinner de carregamento) e devem ser mantidas, apenas desaceleradas.
Transições vs @keyframes — O Guia de Decisão
Se você não tiver certeza qual usar, faça duas perguntas sobre a animação que está construindo:
- É disparada por uma mudança de estado? (hover, focus, toggle de classe, checkbox) → Use uma transição. É exatamente para isso que as transições foram projetadas.
- Faz loop indefinidamente? → Use @keyframes com
animation-iteration-count: infinite. - Tem mais de dois passos? (não apenas início → fim, mas início → meio → fim ou mais) → Use @keyframes.
- Precisa rodar no carregamento da página, sem nenhuma interação do usuário? → Use @keyframes.
- O elemento precisa ficar no estado final após terminar? → Use @keyframes com
animation-fill-mode: forwards.
Conclusão
Transições lidam com movimento guiado por estado. @keyframes lida com todo o resto. Sempre anime transform e opacity para desempenho — deixe width, height, top e left de fora. Use animation-fill-mode: forwards sempre que precisar que o elemento mantenha seu estado final. Adicione o override de prefers-reduced-motion antes de lançar qualquer coisa — é uma adição de dez linhas que importa muito para usuários reais. Para mais sobre o modelo de composição e o que dispara layout, os docs de desempenho de renderização do web.dev são o recurso mais prático disponível. Depois de escrever o CSS, passe pelo CSS Formatter para manter o shorthand legível, ou pelo CSS Minifier para remover espaços em branco antes de lançar para produção.