CSS puro para um quadrado que pula
A parte mais chata não é fazer o quadrado subir e descer. É fazer com que a simulação de gravidade pareça minimamente realista em vez de um quadrado subindo com velocidade constante. A maioria dos tutoriais usa linear e já acha que tá bom. Não é. O jeito mais direto é usar keyframes no CSS, animando de translateY(0) para translateY(-120px) e de volta. O segredo é aplicar uma easing curve cúbica que simule a desaceleração na altura máxima e a aceleração na queda.
quadrado que pula
Veja o código básico: div {
width: 60px;
height: 60px;
background: #4a90e2;
border-radius: 2px;
animation: pular 0.9s cubic-bezier(0.55, 0.06, 0.68, 0.19) infinite;
}
@keyframes pular {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-140px); }
}
O valor do cubic-bezier acima é o que faz o quadrado desacelerar no topo e acelerar na descida. Se você colocar ease-in-out, vai parecer um robô subindo e descendo em linha reta. A diferença é absurda depois que vê lado a lado. Também vale adicionar scaleY durante o impacto. Na interseção entre a subida e a descida, você pode esticar levemente o quadrado para dar a sensação de colisão com o chão.
div.pulo-com-deformacao {
animation: pular 0.9s cubic-bezier(0.55, 0.06, 0.68, 0.19) infinite;
}
@keyframes pular {
0%, 100% { transform: translateY(0) scaleY(1.15); }
50% { transform: translateY(-140px) scaleY(0.88); }
} O scaleY(1.15) no chão e scaleY(0.88) no topo cria a ilusão de peso sem precisar de JavaScript. Usei isso em um painel de dashboard interno e o cliente achou que era motion design profissional. Na verdade era uma diver de 60px com três números em um bezier.
Aqui vai algo que ninguém conta: o problema do transform-origin. Por padrão, a transformação acontece pelo centro do elemento. Quando você aplica scaleY, o quadrado cresce para cima e para baixo ao mesmo tempo, e isso quebra a sensação de impacto. A solução é definir transform-origin: bottom no quadrado. Essa é uma armadilha clássica. Eu perdi umas três horas corrigindo isso num projeto porque o quadrado parecia estar flutuando em vez de quicar. Quando o origin fica no fundo, o scaleY só empurra para cima, que é exatamente o comportamento que seu cérebro espera ver numa bola ou num quadrado quicando.
Quando CSS não resolve
Se você precisa de interatividade — tipo o quadrado pular quando o usuário clica, ou mudar a altura conforme dados vindos de uma API — aí CSS puro vira gambiarra rápido. O ciclo animation-play-state funciona mas é limitado pra controle granular. Num caso real, eu tive que fazer um indicador de carregamento onde o quadrado pulava mais alto conforme o progresso do upload subia. A velocidade do pulo também mudava. CSS sozinho não consegue ler variáveis do backend e mapear isso pra keyframes dinamicamente.
👉 Clique no botão abaixo para saber mais sobre o assunto!
Nesse cenário, o ideal é usar requestAnimationFrame e calcular a posição Y manualmente com uma equação de projétil simples: y = vt - 0.5 * g * t². Você define uma velocidade inicial, aplica uma gravidade fixa (geralmente entre 900 e 1200 px/s² dependendo da escala da tela), e inverte a velocidade toda vez que o quadrado toca o chão.
Esse script funciona em qualquer navegador moderno e o custo de processamento é praticamente zero. O quadrado roda a 60fps sem travar a thread principal porque o DOM só é tocado uma vez por frame.
Problemas que aparecem na prática
O primeiro problema é a inconsistência de fps entre monitores. Em telas de 120Hz ou 144Hz, o quadrado vai pular quase duas vezes mais rápido se você não normalizar o delta time. O código acima já faz isso com performance.now(), então você não tem esse problema. Se usasse setInterval com passo fixo, sim, teria. O segundo problema é mais sutil: o quadrado pode parecer meio quebrado em dispositivos móveis mais antigos porque o GPU não consegue processar transformações compostas com blur ou sombras simultaneamente. Se você colocar box-shadow no quadrado e animar transform ao mesmo tempo, em alguns celulares mais antigos o navegador escala de volta pro CPU e o framerate cai pra 20 ou 30fps.
Minha solução foi simples: remover a sombra e usar um gradiente linear como fake sombreamento dentro do próprio background. O resultado visual é 90% similar e o quadrado que pula roda liso em qualquer aparelho, desde um Samsung Galaxy J de 2017 até um MacBook Pro novo.
Alternativas quando o quadrado que pula não é suficiente
Se o objetivo é só um loader simples, CSS puro com o cubic-bezier correto é a escolha mais leve. Três linhas de CSS, zero dependência, funciona offline. Não precisa importar biblioteca nenhuma. Se você precisa de física mais complexa — colisão entre múltiplos quadrados, atrito, rotação — aí o canvas com Matter.js ou PixiJS faz sentido. Mas aí você já saiu do terreno do quadrado que pula e entrou num simulador de física completo. É overengineering na maioria dos casos.
Também tem a opção de GSAP com Physics2D plugin. É pesado comparado ao requestAnimationFrame manual, mas o ganho em produtividade existe se você já tem GSAP no projeto. O problema é que GSAP adiciona cerca de 65kb minificado ao bundle, o que pode não valer a pena só por um quadrado quicando. Resumindo: CSS puro com bezier e transform-origin no bottom funciona para a grande maioria dos casos. Saia disso só quando precisar de interação em tempo real ou múltiplos objetos colidindo.