Motion slop: el fade-in-up es el text-gray-600 de las animaciones
Cada sección de las landing generadas por IA da el mismo saltito de 16 píxeles al hacer scroll. No es casualidad: es la firma del motion sin criterio. Te explico por qué pasa y cómo animar con intención.
Haz scroll en cualquier landing generada por v0, Lovable o Bolt en los últimos dieciocho meses. El hero ya está ahí. Sigues bajando y la rejilla de features sube 16 píxeles y aparece con un fundido. Bajas un poco más y los testimonios suben 16 píxeles y aparecen con un fundido. Las tarjetas de precios suben 16 píxeles y aparecen con un fundido. El acordeón de FAQ, el CTA del footer, la nube de logos: todos igual, los mismos 16 píxeles, los mismos 500 milisegundos, el mismo ease-out, el mismo disparador al 30% de entrada en el viewport.
Esto es el fade-in-up. Es el text-gray-600 del motion. Y una vez que lo ves, ya no puedes dejar de verlo: cada sección de la página dando el mismo brinquito, como un coro que solo se sabe un paso de baile.
El código exacto, porque siempre es el código exacto
Aquí tienes la versión con Framer Motion. Si has tocado un constructor de IA este año, has publicado esto tal cual:
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" }}
>A veces es y: 20, a veces y: 16, de vez en cuando y: 24. La duración es 0.5 o 0.6. La curva es easeOut, porque es la primera que aparece en la documentación y la que "sienta bien" a un modelo que nunca ha sentido nada. El viewport={{ once: true }} está ahí porque alguien en Stack Overflow se quejó de que la animación se volvía a disparar al subir, y esa respuesta acabó en los datos de entrenamiento.
La versión sin React es AOS — Animate On Scroll — y va todavía más al desnudo:
<div data-aos="fade-up" data-aos-duration="600" data-aos-once="true">O el Intersection Observer hecho a mano que escribe ChatGPT cuando le pides "animaciones de scroll sin librería":
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);
}Tres stacks distintos. Un único resultado idéntico: translateY(20px), 600ms, ease-out, dispara una vez al 10% de umbral. Cambia la librería; la huella no. Esa convergencia — herramientas distintas, mismo artefacto — es la tesis entera de por qué todas las webs generadas por IA se parecen, y el motion es solo la parte que a la gente se le olvidó auditar.
Por qué esta animación concreta se convirtió en la opción por defecto
No es casualidad que la máquina haya aterrizado aquí. El fade-in-up es el mínimo global del "motion seguro", y todas las fuerzas empujan hacia él.
No puede romper el layout. Un empujoncito vertical de 16px sobre la opacidad no provoca reflow, no genera scroll horizontal, no desborda un contenedor, no pelea con el grid. Una IA que optimiza para "renderiza sin errores a la primera" elige la transformación que es imposible hacer mal. Animar width o height arriesga a un layout thrash. Animar scale más allá de cierto punto recorta. translateY(20px) + opacity es la única combinación con cero modos de fallo y, de paso, las dos únicas propiedades que el navegador puede animar fuera del hilo principal en el compositor de la GPU, así que ni siquiera da tirones. La máquina tropezó con la respuesta correcta más barata.
Es literalmente el primer ejemplo de toda la documentación. Abre la home de Framer Motion. La sección de whileInView usa un fundido con subida. La demo de AOS arranca con fade-up. El ejemplo inicial de ScrollTrigger de GSAP es un tween de y con opacity. Los modelos se entrenan con documentación y con los posts de blog que copian la documentación, así que la animación más documentada se convierte en la más generada. Es el mismo mecanismo que hizo que font-sans resolviera a Inter en todas partes, lo cuento en el problema de la tipografía. El default de la documentación se convierte en el default del mundo.
A quien no es diseñador le suena a "premium". Cualquier movimiento parece más cuidado que ningún movimiento para alguien que no sabe explicar por qué. El modelo sabe, estadísticamente, que "landing moderna" coaparece con "scroll reveal", así que añade el reveal. No sabe que el movimiento tiene un trabajo que hacer. Sabe que el movimiento está correlacionado con el adjetivo que pedía el prompt.
Nadie pone objeciones. Quien escribe "hazme una landing de SaaS" no tiene opinión sobre las curvas de easing. Ve que las cosas se mueven, piensa "se ve con vida" y publica. El bucle de feedback que castigaría el movimiento uniforme nunca llega a ejecutarse.
Por qué el movimiento uniforme delata a la máquina
Aquí está lo que se les escapa a los constructores. El problema no es el fade-in-up en sí. Un único fade-up bien colocado en el titular del hero está bien — está incluso bien hecho. El problema es que todos los elementos reciben el mismo.
El diseño de movimiento de verdad es una jerarquía de intención. El titular principal sube y aparece. El subtítulo lo sigue 80ms después — un stagger, porque está subordinado. El botón de CTA no aparece con fundido en absoluto; está ahí desde el primer fotograma, quizá con un asentamiento mínimo de escala, porque lo quieres clicable de inmediato. Una captura de producto entra deslizándose desde un lado porque se está sumando a la conversación. Un contador de números sube tic a tic porque el movimiento *es* la información. Cada decisión responde a una pregunta: ¿cuál es el papel de este elemento, y qué debería decir el movimiento sobre él?
El motion de IA no responde a nada. Aplica una sola transformación a un .map() sobre tus secciones. La tarjeta de testimonio y el aviso legal del footer se animan idéntico porque para el generador ambos son elementos dentro de un array. Esa es la pista. Los humanos diferencian; las máquinas iteran. Cuando el ojo ve que el footer hace la misma entrada que el hero, una parte preconsciente del cerebro lanza la alarma: *aquí no se decidió nada.* Es el equivalente en movimiento de que cada tarjeta sea rounded-2xl con shadow-sm: la uniformidad es la firma. Las 23 señales de código generado por IA cubren la versión estática de esto; el motion es la misma enfermedad en la dimensión temporal.
Hay una segunda pista montada encima: el stagger que no lo es. Cuando la IA sí intenta variedad, escalona un grid con transition={{ delay: index * 0.1 }}. Ahora seis tarjetas de features entran en cascada. Parece más sofisticado. Es peor. Un retardo de 100ms por elemento sobre seis ítems significa que el usuario espera 600ms a que llegue la última tarjeta, y la cascada no tiene ningún significado: la tarjeta 6 no es más importante que la 1, así que ¿por qué llega más tarde? El stagger mecánico es movimiento disfrazado de coreografía. Lo detectas desde la otra punta de la sala porque el ritmo es perfectamente lineal: 0, 100, 200, 300. Nada en el buen diseño se mueve sobre un calendario de retardos perfectamente lineal.
Y el easing. ease-out en todo. ease-out significa arranque rápido, final lento — perfecto para algo que *llega* y se asienta. Pero la IA lo usa también para las salidas, para los hovers, para todo, porque es el default seguro. El movimiento de verdad mezcla: ease-out para las entradas, ease-in para las salidas, un cubic-bezier(0.34, 1.56, 0.64, 1) a medida con sobreimpulso para algo juguetón, lineal solo para bucles continuos. Una página donde todo comparte una única curva de easing tiene el rango tonal de una sola tecla de piano.
Cómo detectarlo en 30 segundos
Abre las DevTools, ve al panel de Animations (en Chrome: More Tools → Animations) y haz scroll. Si todas las animaciones registradas comparten la misma duración, el mismo easing y la misma distancia de transformación, estás ante una página generada. O lee el código fuente: busca en el bundle whileInView, data-aos, o una clase .reveal con translateY. Luego comprueba: ¿está aplicado a *un* elemento con intención, o repartido con un .map() por todo el documento?
La heurística más rápida, sin herramientas: haz scroll a un ritmo de lectura normal y pregúntate si el movimiento te dijo algo. ¿Algo dirigió tu mirada hacia lo que importaba? ¿O la página entera dio una sacudida hacia arriba en secuencia según avanzabas? Si es lo segundo, el movimiento es decoración aplicada por un generador — un punto más en la checklist para detectar una web de IA en 30 segundos, justo al lado del degradado azul-violeta y la fuente Geist.
Una pista más: el parpadeo de carga. Muchos montajes de scroll-reveal con IA ponen opacity: 0 en el CSS pero disparan el reveal en JS. Con una conexión lenta o con JS desactivado, el contenido above the fold es invisible — una página en blanco hasta que el observer se dispara. También hunde el rendimiento medido: cada elemento que aparece de golpe después del primer pintado cuenta para el Cumulative Layout Shift, y una página que esconde su hero detrás de un observer puede marcar un CLS por encima de 0.25 (el umbral "pobre" de Google es 0.1) y perder por completo su candidato a Largest Contentful Paint, porque el elemento LCP no estaba pintado cuando el navegador miró. Una implementación de verdad nunca esconde contenido detrás de un script. La generada publicará encantada una página que está en blanco para el primer pintado de un lector de pantalla y para un rastreo de Lighthouse — que es exactamente el tipo de cosa que la maquinaria de contenido útil de Google está aprendiendo a castigar.
Movimiento con intención
Aquí está el replanteamiento. Antes de añadir una sola transición, responde a una pregunta por cada elemento animado: ¿cuál es el trabajo de este movimiento? Si no sabes nombrar el trabajo, borra la animación. "Queda bonito" no es un trabajo.
Cuando el movimiento tiene un trabajo, el código deja de ser uniforme por sí solo, porque trabajos distintos necesitan movimientos distintos:
// Trabajo: dirigir la mirada a la afirmación principal y luego
// dejar que la línea de apoyo la siga como subordinada. Un stagger, intencional.
<motion.h1
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: [0.22, 1, 0.36, 1] }}
>
Reduce tu factura de AWS a la mitad.
</motion.h1>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4, delay: 0.12 }} // sigue al titular
>
Auditamos las instancias ociosas que olvidaste tener encendidas.
</motion.p>
// Trabajo: ninguno. El CTA es el objetivo. Está ahí desde el primer fotograma.
<a href="/start" className="btn-primary">Empieza la auditoría</a>Fíjate en qué cambia. El titular usa una curva de easing de verdad — [0.22, 1, 0.36, 1], una desaceleración rápida, no el perezoso easeOut. El subtítulo solo se funde, sin y, porque no está haciendo una entrada, está poniéndose al día. El CTA no se anima en absoluto. Tres elementos, tres decisiones. Eso es lo contrario de un .map().
Para el scroll, la regla es: revela algo que el usuario no podía ver de todas formas, y revélalo porque verlo importa ahora. Una gráfica que dibuja sus barras al entrar en el viewport — el movimiento *es* el dato revelándose a sí mismo, ganado. Una tabla de precios que no hace nada al hacer scroll porque es una tabla y quieres leerla, no mirarla — también correcto. El instinto generado es "todo se revela". El instinto diseñado es "casi nada se revela, y lo que sí lo hace tiene un motivo".
Y haz la versión accesible como toca, en CSS, para que el contenido nunca quede tras una verja de JS y los usuarios con movimiento reducido reciban el contenido sin saltos:
.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; }
}Ese último bloque es la línea que el generador casi nunca escribe. Aproximadamente uno de cada tres usuarios de macOS e iOS tiene "Reducir movimiento" activado en algún momento; publicar sin esa media query significa que tu reveal "premium" es, para ellos, solo un layout que pega tirones.
Algunos movimientos prácticos anti-slop:
- Varía por papel, no por índice. Nada de
delay: i * 0.1. Escalona solo cuando los elementos forman una secuencia genuina — un proceso numerado, una línea temporal. Para un grid de tarjetas con el mismo peso, funde el grupo entero a la vez, o no lo fundas en absoluto. - Mezcla tus easings. Las entradas desaceleran. Las salidas aceleran. Reserva el sobreimpulso (
cubic-bezier(0.34, 1.56, 0.64, 1)) para uno o dos momentos juguetones, para que siga siendo especial. Una página con tres curvas distintas se nota escrita por alguien; una página con una sola se nota impresa. - Anima una propiedad inesperada. Todo el mundo hace opacity y translate. Un barrido con
clip-path, unfilter: blur(8px)asentándose hasta la nitidez, un único acento que pasa de#64748ba#0ea5e9— esto se lee como deliberado precisamente porque el generador nunca echa mano de ello. - Mata el reveal below the fold en contenido largo. Un artículo o una página de documentación no necesita que sus párrafos suban según haces scroll. Eso es movimiento por el movimiento, y encima ralentiza la lectura activamente.
- Nunca escondas contenido detrás de JS. El movimiento es una capa de mejora, no una verja. Visible en el primer pintado, siempre.
El argumento de fondo recorre este blog entero. El default de la IA no es malo por feo — el fade-in-up es perfectamente agradable durante medio segundo. Es malo porque es *indiferenciado*, y lo indiferenciado es la verdadera huella de la salida de una máquina. El degradado, la fuente, la tarjeta rounded-2xl, el cuerpo en text-gray-600 y el scroll reveal de 16px son el mismo fallo: un generador eligiendo la opción segura del mínimo global y aplicándola en todas partes, porque no tiene gusto que arriesgar ni intención que expresar.
Se rompe siempre de la misma manera. Deja de dejar que decida la herramienta. Coge cada elemento y pregúntate para qué sirve. El movimiento que sobreviva a esa pregunta es el movimiento que vale la pena publicar — y nunca, jamás, parecerá un .map() de whileInView cayendo por la 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.