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.
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:
- O elemento está entrando ou saindo da tela? Use
ease-out. Menus, modais, toasts, popovers. - 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. - É uma mudança de cor ou um hover? Use
ease. - É 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:
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)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:
| Elemento | Duração |
|---|---|
| Feedback de clique em botão | 100 a 160ms |
| Tooltips e popovers pequenos | 125 a 200ms |
| Dropdowns e selects | 150 a 250ms |
| Modais e drawers | 200 a 500ms |
| Animações explicativas, de marketing | podem 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:
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-outpara entrar e sair,ease-in-outpara mover,easepara cor,linearpara movimento contínuo.- Nunca
ease-inem 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.
.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);
}
- 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);
}var(--ease-out)no lugar deease-in. O menu está entrando na tela, então a resposta vem no primeiro frame.- 200ms em vez de 400ms. Dropdown fica entre 150 e 250ms.
transform-origin: top center. O menu cresce a partir do botão, e o olho entende de onde ele veio.scale(0.96)em vez descale(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.
.btn {
transition: all 300ms linear;
}
.btn:hover {
background: var(--btn-hover);
}
.btn:active {
transform: scale(0.85);
}
- 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);
}- Uma transição por propriedade, nunca
all. Você decide o que anima e o navegador não trabalha à toa. easena cor. É mudança de cor, não movimento, e a curva padrão serve.var(--ease-out)no aperto, em 160ms. É feedback de clique: precisa ser imediato.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.
.bar {
width: 0%;
transition: width 3s linear;
}
.bar.open {
width: 100%;
}
- 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;
}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.linearcontinua 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.- 300ms. Curto o bastante pra alcançar o número antes do próximo pedaço. Mais que isso e a barra vive atrasada.
transform: scaleX()em vez dewidth. Otransformroda no compositor, sem recalcular layout a cada frame.- 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.