A curva certa

Duas animações podem durar exatamente o mesmo tempo e uma parecer muito mais rápida que a outra. A diferença está na curva: o easing decide quando o movimento acontece dentro daquele tempo.

É a escolha que mais muda a sensação de uma animação, e quase sempre é feita no automático. Esta aula é sobre fazer essa escolha de propósito.

O mesmo tempo, sensações diferentes

Os dois menus abaixo duram 300ms. O da esquerda usa ease-in, o da direita usa um ease-out forte. Abra os dois juntos.

ease-in
300ms
ease-out forte
300ms
Mesma duração. O da direita parece ter respondido antes.

O ease-in começa devagar. Nos primeiros frames quase nada acontece, e é justo nesse momento que o usuário está olhando: ele acabou de clicar e espera uma resposta. O ease-out faz o contrário. A maior parte do movimento acontece logo no começo, e o resto é o pouso.

Por isso o ease-out parece mais rápido mesmo sem ser. A percepção de velocidade vem do primeiro instante, não do total.

Qual curva usar

Na prática a decisão cabe em quatro perguntas, nesta ordem:

  1. O elemento está entrando ou saindo da tela? Use ease-out. Menus, modais, toasts, popovers.
  2. Ele já está na tela e vai se mover ou mudar de forma? Use ease-in-out. Acelera, depois freia, como um objeto de verdade.
  3. É uma mudança de cor ou um hover? Use ease.
  4. É um movimento contínuo, como uma barra de progresso ou um marquee? Use linear.

Se nenhuma se aplica, fique com ease-out.

Antes de seguir, teste. Escolha a curva de cada situação:

Qual curva?0/4
Um menu abrindo depois do clique
Um card trocando de lugar numa lista
A cor de um link no hover
A barra de progresso de um upload

As curvas do navegador são fracas

O problema é que ease-out e ease-in-out do CSS são tímidos demais. A diferença entre o início e o fim do movimento é pequena, e a animação acaba sem a firmeza que faz ela parecer intencional.

Compare as curvas abaixo. A linha pontilhada é o linear: quanto mais a curva se afasta dela no começo, mais rápida é a resposta.

cubic-bezier(0.23, 1, 0.32, 1)
Resposta imediata, pouso suave. O padrão para entrar e sair.

Em vez de desenhar curvas do zero, deixe três prontas como variáveis e use sempre as mesmas:

:root {
  /* Entrar e sair: resposta imediata, pouso suave */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
 
  /* Mover o que já está na tela */
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
 
  /* Drawers e sheets, a curva do iOS */
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
}

Usar poucas curvas, e sempre as mesmas, faz a interface inteira se mover com uma voz só. Se precisar de outras, o easing.dev e o easings.co têm versões mais fortes das curvas padrão.

Quanto tempo

Com a curva certa, a duração pode ser menor do que você imagina. Uma boa referência:

ElementoDuração
Feedback de clique em botão100 a 160ms
Tooltips e popovers pequenos125 a 200ms
Dropdowns e selects150 a 250ms
Modais e drawers200 a 500ms
Animações explicativas, de marketingpodem ser mais longas

Animação de interface deve ficar abaixo de 300ms. Os dois menus abaixo usam a mesma curva e mudam só a duração:

400ms
180ms
Mesma curva. O de 180ms parece parte do clique; o de 400ms parece uma espera.

O mesmo vale fora dos menus. Um spinner que gira mais rápido faz o carregamento parecer mais curto, mesmo com o tempo de espera idêntico. A velocidade que o usuário sente importa tanto quanto a velocidade real.

Resumo

  • A curva decide quando o movimento acontece. Ela muda a sensação mais do que a duração.
  • ease-out para entrar e sair, ease-in-out para mover, ease para cor, linear para movimento contínuo.
  • Nunca ease-in em interface.
  • Troque as curvas do navegador por versões mais fortes e reutilize sempre as mesmas.
  • Interface abaixo de 300ms.

Pratique

Três interfaces com a curva errada. Em cada uma, mexa no styles.css até os critérios ficarem todos marcados. O preview muda enquanto você digita, então clique nele a cada ajuste e sinta a diferença.

As curvas da aula já estão prontas como variáveis: var(--ease-out), var(--ease-in-out) e var(--ease-drawer).

Conserte o dropdown

Este menu faz quase tudo errado: começa do nada, cresce a partir do centro e demora a responder.

Resultado esperado. Clique e sinta.
.menu {
  opacity: 0;
  transform: scale(0);
  transform-origin: center;
  transition: opacity 400ms ease-in, transform 400ms ease-in;
}

.menu.open {
  opacity: 1;
  transform: scale(1);
}
Critérios0/5
  • Nenhuma transição com ease-in(pendente)
  • Uma curva ease-out forte(pendente)
  • Duração entre 150 e 250ms(pendente)
  • Abre a partir do botão (transform-origin no topo)(pendente)
  • Começa de pelo menos scale(0.9), nunca do zero(pendente)
Mostrar dica

São quatro coisas: a curva, a duração, de onde ele cresce e de que tamanho ele parte. Um menu nasce do botão que o abriu, não do meio de si mesmo.

Ver solução
.menu {
  opacity: 0;
  transform: scale(0.96);
  transform-origin: top center;
  transition:
    opacity 200ms var(--ease-out),
    transform 200ms var(--ease-out);
}
  1. var(--ease-out) no lugar de ease-in. O menu está entrando na tela, então a resposta vem no primeiro frame.
  2. 200ms em vez de 400ms. Dropdown fica entre 150 e 250ms.
  3. transform-origin: top center. O menu cresce a partir do botão, e o olho entende de onde ele veio.
  4. scale(0.96) em vez de scale(0). Nada no mundo real aparece do zero; começar quase do tamanho final deixa a entrada suave.

Um hover que não enrola

O botão responde devagar, anima tudo o que muda e afunda demais no clique.

Resultado esperado. Clique e sinta.
.btn {
  transition: all 300ms linear;
}

.btn:hover {
  background: var(--btn-hover);
}

.btn:active {
  transform: scale(0.85);
}
Critérios0/5
  • Sem transition: all, só as propriedades que mudam(pendente)
  • A cor muda com ease(pendente)
  • Hover rápido, até 200ms(pendente)
  • O aperto usa ease-out forte, até 160ms(pendente)
  • Aperto sutil: scale(0.96)(pendente)
Mostrar dica

Separe as duas transições: a cor e o aperto têm curvas diferentes. E transition: all anima coisas que você nem pretendia.

Ver solução
.btn {
  transition:
    background-color 150ms ease,
    transform 160ms var(--ease-out);
}
 
.btn:active {
  transform: scale(0.96);
}
  1. Uma transição por propriedade, nunca all. Você decide o que anima e o navegador não trabalha à toa.
  2. ease na cor. É mudança de cor, não movimento, e a curva padrão serve.
  3. var(--ease-out) no aperto, em 160ms. É feedback de clique: precisa ser imediato.
  4. scale(0.96). O suficiente para confirmar o clique, sem parecer que o botão afundou.

A barra do download

Esta barra devia acompanhar o download de um arquivo pesado. Os dados chegam em rajadas, como na vida real: devagar, devagar, uma rajada, devagar de novo.

Clique em "Baixar arquivo" e compare os dois. Na sua versão, a barra finge: sobe de 0 a 100% em linha reta, em 3 segundos, sem olhar pro download, e o número finge junto. O arquivo ainda está chegando e ela já terminou. No resultado esperado, a barra e o número seguem cada rajada e param quando os dados param.

Resultado esperado. Clique e sinta.
.bar {
  width: 0%;
  transition: width 3s linear;
}

.bar.open {
  width: 100%;
}
Critérios0/4
  • Continua linear, sem acelerar e frear(pendente)
  • Transição curta, até 400ms, pra alcançar cada pedaço(pendente)
  • Anima transform, não width(pendente)
  • Segue o download de verdade: scaleX(var(--progress))(pendente)
Mostrar dica

O progresso real já está no elemento, na variável --progress (de 0 a 1). Em vez de animar até o fim por conta própria, a barra tem que seguir esse valor, com uma transição curta o bastante pra alcançar cada pedaço antes do próximo. E animar width recalcula o layout a cada frame.

Ver solução
.bar {
  transform: scaleX(var(--progress));
  transition: transform 300ms linear;
}
  1. scaleX(var(--progress)). A barra mostra o progresso de verdade, não uma animação de 3 segundos que termina antes do arquivo. O número ao lado lê a barra, então passa a mostrar o download real junto com ela.
  2. linear continua certo, só que agora em cada pedaço. Com qualquer outra curva a barra aceleraria e frearia a cada rajada, e o progresso pareceria pulsar.
  3. 300ms. Curto o bastante pra alcançar o número antes do próximo pedaço. Mais que isso e a barra vive atrasada.
  4. transform: scaleX() em vez de width. O transform roda no compositor, sem recalcular layout a cada frame.
  5. O ritmo irregular é o certo: ele vem dos dados, não da curva. A curva só não pode inventar movimento que o download não teve.

Créditos

Esta aula adapta o framework de decisão de animação de Emil Kowalski, publicado sob licença MIT. A curva de drawer vem do Ionic Framework.