Motion slop: o fade-in-up é o text-gray-600 das animações de scroll
Toda seção sobe 16 pixels e dá um fade ao entrar na viewport: mesma duração, mesmo easing, mesmo gatilho. Esse é o motion slop da IA, e dá pra reconhecer em 30 segundos.
Faça scroll em qualquer landing page gerada por v0, Lovable ou Bolt nos últimos dezoito meses. O hero já está lá. Aí você rola a página, e o grid de features sobe 16 pixels e dá um fade. Rola mais um pouco: os depoimentos sobem 16 pixels e dão um fade. Os cards de preço sobem 16 pixels e dão um fade. O acordeão de FAQ, o CTA do rodapé, a nuvem de logos: todos eles, os mesmos 16 pixels, os mesmos 500 milissegundos, o mesmo ease-out, o mesmo gatilho a 30% de entrada na viewport.
Isso é o fade-in-up. É o text-gray-600 do motion. E depois que você enxerga, não consegue mais desenxergar: cada seção da página dando o mesmo pulinho, feito uma fila de coristas que só sabe um passo.
O código exato, porque é sempre o código exato
Aqui está a versão em Framer Motion. Se você usou algum builder de IA neste ano, já mandou isso pra produção sem mudar uma vírgula:
const fadeInUp = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
<motion.section
variants={fadeInUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ duration: 0.5, ease: "easeOut" }}
>Às vezes y: 20, às vezes y: 16, de vez em quando y: 24. A duração é 0.5 ou 0.6. O easing é easeOut, porque é o primeiro easing da documentação e o que "parece agradável" pra um modelo que nunca sentiu nada. viewport={{ once: true }} está ali porque alguém no Stack Overflow reclamou que a animação disparava de novo ao rolar pra cima, e essa resposta está nos dados de treino.
A versão sem React é o AOS — Animate On Scroll — e é ainda mais escancarada:
<div data-aos="fade-up" data-aos-duration="600" data-aos-once="true">Ou o Intersection Observer feito na unha que o ChatGPT escreve quando você pede "animações de scroll sem biblioteca":
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("animate-in");
}
});
}, { threshold: 0.1 });
document.querySelectorAll(".reveal").forEach((el) => observer.observe(el));.reveal {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.reveal.animate-in {
opacity: 1;
transform: translateY(0);
}Três stacks diferentes. Um resultado idêntico: translateY(20px), 600ms, ease-out, dispara uma vez num threshold de 10%. A biblioteca muda; a impressão digital não. Essa convergência — ferramentas diferentes, artefato igual — é a tese inteira de por que todo site gerado por IA parece o mesmo, e o motion é só a parte que esqueceram de auditar.
Por que essa animação específica virou o default
Não é por acaso que a máquina parou aqui. O fade-in-up é o mínimo global do "motion seguro", e todas as forças empurram pra ele.
Não tem como quebrar o layout. Um empurrãozinho vertical de 16px com opacity não causa reflow, não dispara scrollbar horizontal, não estoura container, não briga com o grid. Uma IA otimizando pra "renderizar sem erro na primeira tentativa" escolhe o transform que é impossível de errar. Animar width ou height arrisca um layout thrash. Animar scale além de um certo ponto corta o elemento. translateY(20px) + opacity é a única combinação com zero modos de falha — e, convenientemente, as duas únicas propriedades que o navegador consegue animar fora da main thread, no compositor da GPU, então nem dá engasgo. A máquina tropeçou na resposta correta mais barata.
É literalmente o primeiro exemplo de toda documentação. Abra a home do Framer Motion. A seção do whileInView usa um fade-and-rise. A demo do AOS começa com fade-up. O starter do ScrollTrigger do GSAP é um tween de y com opacity. Os modelos são treinados em documentação e nos posts de blog que copiam documentação, então a animação mais documentada vira a mais gerada. É o mesmo mecanismo que fez font-sans resolver pra Inter em todo lugar, descrito em o problema da tipografia. O default da doc vira o default do mundo.
Pra quem não é designer, soa "premium". Qualquer motion parece mais caprichado do que motion nenhum pra quem não consegue articular o porquê. O modelo sabe, estatisticamente, que "landing page moderna" coocorre com "scroll reveal", então ele adiciona o reveal. Ele não sabe que motion tem uma função. Ele sabe que motion está correlacionado com o adjetivo que o prompt pediu.
Ninguém contesta. Quem digita "faça uma landing page de SaaS pra mim" não tem opinião nenhuma sobre curva de easing. A pessoa vê as coisas se mexendo, pensa "parece vivo" e manda pra produção. O ciclo de feedback que puniria o motion uniforme nunca roda.
Por que motion uniforme grita "feito por máquina"
Aqui está a parte que os builders não pegam. O problema não é o fade-in-up em si. Um único fade-up bem colocado num headline de hero é ok — é bom, até. O problema é que cada elemento ganha o mesmo.
Design de motion de verdade é uma hierarquia de intenção. O headline principal sobe e dá fade. O subtítulo segue 80ms depois — um stagger, porque é subordinado. O botão de CTA não dá fade nenhum; ele está ali desde o primeiro frame, talvez com um pequeno assentamento de scale, porque você quer ele clicável imediatamente. Um screenshot do produto desliza pela lateral porque está entrando na conversa. Um contador de número vai subindo porque o motion *é* a informação. Cada escolha responde a uma pergunta: qual é o papel desse elemento, e o que o motion deveria dizer sobre ele?
O motion da IA não responde pergunta nenhuma. Ele aplica um transform num .map() sobre as suas seções. O card de depoimento e o rodapé legal animam de forma idêntica porque, pro gerador, ambos são elementos num array. Esse é o sinal. Humanos diferenciam; máquinas iteram. Quando o olho vê o rodapé fazendo a mesma entrada do hero, alguma parte pré-consciente do cérebro acende um alerta: *nada aqui foi decidido.* É o equivalente em motion de todo card ser rounded-2xl com shadow-sm — a uniformidade é a assinatura. Os 23 sinais de código gerado por IA cobrem a versão estática disso; o motion é a mesma doença na dimensão do tempo.
Tem um segundo sinal sobreposto: o stagger que não é stagger. Quando a IA tenta variar, ela escalona um grid com transition={{ delay: index * 0.1 }}. Agora seis cards de feature entram em cascata. Parece mais chique. É pior. Um delay de 100ms por item em seis itens significa que o usuário espera 600ms pelo último card, e a cascata não significa nada — o card 6 não é mais importante que o card 1, então por que ele chega depois? Stagger mecânico é motion fantasiado de coreografia. Dá pra reconhecer do outro lado da sala porque o ritmo é perfeitamente linear: 0, 100, 200, 300. Nada em bom design se move num cronograma de delay perfeitamente linear.
E o easing. ease-out em tudo. ease-out significa começa rápido, termina devagar — certo pra algo que está *chegando* e se assentando. Mas a IA usa pra saídas também, pra hovers, pra tudo, porque é o default seguro. Motion de verdade mistura: ease-out pra entradas, ease-in pra saídas, um cubic-bezier(0.34, 1.56, 0.64, 1) customizado com overshoot pra algo brincalhão, linear só pra loops contínuos. Uma página onde tudo compartilha uma curva de easing tem a amplitude tonal de uma única tecla de piano.
Como identificar em 30 segundos
Abra o DevTools, vá no painel de Animations (Chrome: More Tools → Animations) e role a página. Se cada animação gravada compartilha a mesma duração, o mesmo easing e a mesma distância de transform, você está olhando pra uma página gerada. Ou leia o source — procure no bundle por whileInView, data-aos ou uma classe .reveal com translateY. Aí confira: foi aplicado a *um* elemento com intenção, ou deram .map() no documento inteiro?
A heurística mais rápida, sem ferramenta nenhuma: role num ritmo normal de leitura e se pergunte se o motion te disse alguma coisa. Algo puxou seu olho pra o que importava? Ou a página inteira deu um tranco pra cima em sequência conforme você descia? Se for o segundo caso, o motion é decoração aplicada por um gerador — mais um item no checklist de detecção de IA em 30 segundos, bem do lado do gradiente azul-roxo e da fonte Geist.
Mais uma entrega-leve: o flash no carregamento. Muitos setups de scroll-reveal de IA colocam opacity: 0 no CSS mas disparam o reveal no JS. Numa conexão lenta ou com o JS desativado, o conteúdo acima da dobra fica invisível — uma página em branco até o observer disparar. Isso também destrói a performance medida: cada elemento que aparece depois do first paint conta pro Cumulative Layout Shift, e uma página que esconde o hero atrás de um observer pode registrar um CLS acima de 0.25 (o limite "ruim" do Google é 0.1) e perder de vez o candidato a Largest Contentful Paint, porque o elemento de LCP não tinha sido pintado quando o navegador olhou. Uma implementação de verdade nunca esconde conteúdo atrás de um script. A gerada vai alegremente mandar pra produção uma página que está em branco pro first paint de um leitor de tela e pra um crawl do Lighthouse — exatamente o tipo de coisa que a máquina de helpful content do Google está aprendendo a punir.
Motion que tem intenção
Aqui está a virada de chave. Antes de adicionar uma única transição, responda uma pergunta por elemento animado: qual é a função desse motion? Se você não consegue nomear a função, apague a animação. "Fica bonito" não é função.
Quando o motion tem função, o código deixa de ser uniforme por conta própria, porque funções diferentes precisam de motion diferente:
// Job: direct the eye to the primary claim, then let the
// supporting line follow as subordinate. One stagger, intentional.
<motion.h1
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: [0.22, 1, 0.36, 1] }}
>
Cut your AWS bill in half.
</motion.h1>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4, delay: 0.12 }} // follows the headline
>
We audit idle instances you forgot you turned on.
</motion.p>
// Job: none. The CTA is the point. It's there from frame one.
<a href="/start" className="btn-primary">Start the audit</a>Repare no que é diferente. O headline usa uma curva de easing de verdade — [0.22, 1, 0.36, 1], uma desaceleração rápida, não o preguiçoso easeOut. O subtítulo só dá fade, sem y, porque ele não está fazendo uma entrada, está alcançando o headline. O CTA não anima nada. Três elementos, três decisões. Isso é o oposto de um .map().
Pra scroll, a regra é: revele algo que o usuário não conseguiria ver de outra forma, e revele porque vê-lo agora importa. Um gráfico que desenha as barras conforme entra na viewport — o motion *é* o dado se revelando, é merecido. Uma tabela de preços que não faz nada no scroll porque é uma tabela e você quer ler, não assistir — também está certo. O instinto gerado é "tudo se revela". O instinto desenhado é "quase nada se revela, e o que se revela tem um motivo".
E faça a versão acessível direito, no CSS, pra que o conteúdo nunca fique refém do JS e quem usa reduced-motion receba o conteúdo sem nenhum pulo:
.reveal { opacity: 0; transform: translateY(20px); transition: opacity .5s, transform .5s; }
.reveal.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
.reveal { opacity: 1; transform: none; transition: none; }
}Aquele último bloco é a linha que o gerador quase nunca escreve. Mais ou menos um em cada três usuários de macOS e iOS tem o "Reduzir movimento" ligado em algum momento; mandar pra produção sem essa media query significa que o seu reveal "premium" é, pra essas pessoas, só um layout que dá um solavanco.
Alguns movimentos práticos anti-slop:
- Varie por papel, não por índice. Não faça
delay: i * 0.1. Use stagger só quando os elementos formam uma sequência de verdade — um processo numerado, uma timeline. Pra um grid de cards de peso igual, dê fade no grupo inteiro junto, ou não dê fade nenhum. - Misture os easings. Entradas desaceleram. Saídas aceleram. Reserve o overshoot (
cubic-bezier(0.34, 1.56, 0.64, 1)) pra um ou dois momentos brincalhões, pra ele continuar especial. Uma página com três curvas distintas parece autoral; uma página com uma só parece impressa. - Anime uma propriedade inesperada. Todo mundo faz opacity e translate. Um wipe com
clip-path, umfilter: blur(8px)se assentando até ficar nítido, um único detalhe de cor passando de#64748bpra#0ea5e9— isso soa deliberado justamente porque o gerador nunca recorre a essas coisas. - Mate o reveal abaixo da dobra em conteúdo longo. Um artigo ou uma página de docs não precisa dos parágrafos subindo conforme você rola. Isso é motion por motion, e atrapalha ativamente a leitura.
- Nunca esconda conteúdo atrás do JS. Motion é uma camada de enriquecimento, não um portão. Visível no first paint, sempre.
O ponto mais profundo atravessa este blog inteiro. O default da IA não é ruim porque é feio — o fade-in-up é perfeitamente agradável por meio segundo. Ele é ruim porque é *indiferenciado*, e indiferenciado é a verdadeira impressão digital da produção de máquina. O gradiente, a fonte, o card rounded-2xl, o corpo de texto text-gray-600 e o scroll reveal de 16px são a mesma falha: um gerador escolhendo a opção segura de mínimo global e aplicando em todo lugar, porque não tem gosto pra arriscar nem intenção pra expressar.
Você quebra isso sempre da mesma forma. Pare de deixar a ferramenta decidir. Pegue cada elemento e pergunte pra que ele serve. O motion que sobrevive a essa pergunta é o motion que vale a pena mandar pra produção — e ele nunca, jamais, vai parecer um .map() de whileInView descendo pela página.
SHIP CODE THAT LOOKS INTENTIONAL
Scan your frontend for AI patterns. Generate a unique design system. Stop shipping the same blue gradient as everyone else.