12k
All articles

Activar animaciones CSS al hacer scroll sin JavaScript

Crea animaciones CSS activadas al desplazarse, sin JavaScript. Consulta la sintaxis, las acciones, los límites y la alternativa con IntersectionObserver.

OpenReplay Team
OpenReplay Team
Activar animaciones CSS al hacer scroll sin JavaScript

Las animaciones activadas por scroll (scroll-triggered animations) permiten que el desplazamiento decida cuándo comienza una animación CSS normal. Una vez que un elemento alcanza una posición de scroll determinada, la animación se reproduce con su propio reloj. Se declaran con las propiedades timeline-trigger y animation-trigger, sin necesidad de un IntersectionObserver.

La mayor parte del código para hacer un “fade in al hacer scroll” es igual en todas partes: un observer, un umbral, un cambio de clase y una llamada a unobserve() que alguien olvida añadir. Ahora todo ese trabajo puede hacerse en CSS.

Este artículo cubre la sintaxis, un ejemplo completo de aparición, los tres comportamientos del trigger, cómo elegir los límites de scroll y cómo desplegar la funcionalidad de forma segura en un escenario en el que solo la soporta Chromium.

Puntos clave

  • Una animación activada por scroll necesita dos declaraciones. timeline-trigger asigna un nombre a un trigger y define el rango de scroll que lo activa. animation-trigger indica a una animación qué trigger debe seguir y qué hacer cuando se dispara.
  • El rango entry 100% exit 0% activa un trigger solo mientras el elemento está completamente dentro del scrollport. La documentación de Chrome lo considera equivalente a la palabra clave contain.
  • play-forwards play-backwards reproduce la animación al entrar y la invierte al salir. play-once, del borrador de Animation Triggers, la ejecuta una sola vez.
  • Una animación activada por trigger conserva su propia duración y easing, por lo que hacer scroll más rápido no hace que se reproduzca más rápido.
  • Los datos de compatibilidad de Chrome solo incluyen navegadores basados en Chromium para las animaciones activadas por scroll, así que oculta el contenido dentro de @supports y mantén un fallback con IntersectionObserver.

¿Cuál es la diferencia entre las animaciones impulsadas por scroll y las activadas por scroll?

Las animaciones impulsadas por scroll (scroll-driven) vinculan el progreso de una animación a la posición de scroll. Las animaciones activadas por scroll (scroll-triggered) usan el desplazamiento únicamente para decidir cuándo empieza una animación ordinaria. Nuestra guía sobre cómo crear animaciones de scroll solo con CSS cubre el primer modelo: animation-timeline con scroll() y view(). En ese caso, el timeline sustituye al reloj, de modo que al hacer scroll hacia atrás la animación se reproduce a la inversa. Este artículo trata timeline-trigger y animation-trigger, que inician una animación normal, con su propia duración, cuando el elemento alcanza un rango de scroll determinado.

Si quieres una barra de progreso de lectura o un efecto parallax, usa animaciones de progreso de scroll en CSS. Si quieres que una tarjeta aparezca con un fundido de 400ms en cuanto sea visible, usa un trigger. En su resumen de Web UI del Google I/O 2026, Chrome presenta ambas como funcionalidades independientes y describe los triggers como una forma de reemplazar IntersectionObserver con CSS. En pocas palabras, un trigger es la versión CSS de “añadir una clase cuando este elemento se vuelva visible”.

Ejemplo de fade-in activado por scroll

Un fade-in activado por scroll necesita tres partes: una regla @keyframes, una animation con relleno forwards y un trigger que vincule la animación a un view timeline. El anuncio de Chrome sobre las animaciones activadas por scroll define la sintaxis. Sus puntos principales son que el trigger y la animación pueden estar en elementos distintos, y que un trigger utiliza como origen un scroll progress timeline o un view progress timeline.

<section class="reveal">
  <div class="reveal-content">
    <h2>Shipping notes</h2>
    <p>This block fades and slides in once it is fully on screen.</p>
  </div>
</section>
@keyframes reveal {
  to {
    opacity: 1;
    translate: 0 0;
  }
}

@supports (timeline-trigger-name: --t) {
  @media (prefers-reduced-motion: no-preference) {
    .reveal {
      timeline-trigger: --reveal view() entry 100% exit 0%;
      trigger-scope: --reveal;
    }

    .reveal-content {
      opacity: 0;
      translate: 0 2rem;
      animation: reveal 400ms ease-out forwards;
      animation-trigger: --reveal play-forwards;
    }
  }
}

Cómo funciona:

  • El trigger está en la sección y el movimiento en su hijo. El elemento que se desplaza no es el elemento cuya visibilidad se mide, por lo que el desplazamiento de translate no puede mover el límite del trigger.
  • entry 100% exit 0% activa el trigger cuando la sección está completamente dentro del scrollport.
  • play-forwards inicia la animación cada vez que se activa el trigger. El relleno forwards mantiene después el último keyframe.
  • El estado inicial oculto está dentro de la feature query. Los navegadores sin soporte nunca aplican opacity: 0, así que el contenido permanece visible.
  • trigger-scope mantiene local el nombre --reveal de cada sección. Por defecto, cualquier elemento de la página puede ver el nombre de un trigger, igual que ocurre con anchor-name, como se describe en las notas de la versión de Chrome 146.

Solo con play-forwards, salir del rango no tiene ningún efecto: el elemento conserva su estado final mientras te alejas haciendo scroll. Cuando vuelve a ser completamente visible, el trigger se activa de nuevo y la animación se repite. La siguiente sección muestra cómo cambiar este comportamiento.

Elegir la acción del trigger

animation-trigger acepta un nombre de trigger seguido de hasta dos acciones. La primera se ejecuta cuando el trigger se activa y la segunda cuando se desactiva. La acción que elijas determina qué ocurre al volver a entrar y al salir.

Comportamiento deseadoValor de animation-triggerNota
Repetir en cada nueva entrada--reveal play-forwardsPuede saltar visiblemente al estado inicial
Invertir al salir--reveal play-forwards play-backwardsTras invertirse, el elemento muestra sus estilos base
Ejecutar una vez y mantener--reveal play-oncePalabra clave en borrador, puede cambiar
animation-trigger: --reveal play-forwards play-backwards; /* reverses on exit */
animation-trigger: --reveal play-once; /* runs once; Animation Triggers draft keyword */

El relleno forwards mantiene el keyframe to después de que termine play-forwards. No mantiene nada cuando termina play-backwards. En ese momento, la animación deja de aplicarse y el elemento vuelve a sus estilos base. En este ejemplo, los estilos base son el estado inicial oculto (opacity: 0; translate: 0 2rem), ya que los keyframes solo definen to, por lo que el elemento termina igualmente oculto. Si tus keyframes definen un estado from distinto de los estilos base, usa both para que ese estado se mantenga tras la inversión. play-once procede del Editor’s Draft de CSS Animation Triggers Level 1, no del anuncio de Chrome, así que compruébalo en la versión de Chrome a la que te diriges antes de depender de él. Es lo más parecido a llamar a unobserve() tras la primera intersección.

Rango de activación frente a rango activo

En un trigger de scroll, el rango de activación determina cuándo se dispara el trigger. El rango activo opcional, escrito después de una barra, determina durante cuánto tiempo permanece activo, y Chrome exige que englobe el rango de activación. Según Chrome, entry 100% exit 0% equivale a contain y entry 0% exit 100% equivale a cover, por lo que estas dos líneas son idénticas:

timeline-trigger: --reveal view() entry 100% exit 0% / entry 0% exit 100%;
timeline-trigger: --reveal view() contain / cover;

Con esta configuración, el trigger se activa cuando el elemento está completamente a la vista y permanece activo hasta que ninguna parte del elemento es visible. Esto es importante para play-backwards, porque la animación inversa espera hasta que el elemento sale de la pantalla.

Elegir límites de scroll adecuados

El rango de trigger adecuado depende del tamaño del elemento y de cualquier elemento fijo de la interfaz que cubra el viewport.

SituaciónRangoMotivo
Tarjeta o encabezado pequeñoentry 100% exit 0% (contain)Se dispara cuando todo el elemento es visible
Sección altaentry 25% exit 0%contain puede dispararse tarde en elementos altos
Bajo un header sticky de 4remview(block 4rem 0)Evita que el header oculte el elemento
/* Tall element: fire when a quarter has entered */
timeline-trigger: --reveal view() entry 25% exit 0%;

/* 4rem sticky header: start (top) inset first */
timeline-trigger: --reveal view(block 4rem 0) entry 100% exit 0%;

En view(), el inset inicial va antes que el inset final. En un eje de bloque vertical, el inicio es la parte superior, así que la altura de un header superior va primero.

Compatibilidad con navegadores y fallbacks

El resumen de Web UI del Google I/O 2026 de Chrome indica compatibilidad con las animaciones activadas por scroll en Chrome y Edge 146, sin soporte en Firefox ni Safari. El anuncio de Chrome de diciembre de 2025 decía que la funcionalidad llegaría en Chrome 145, pero finalmente se lanzó en Chrome 146 (versión estable del 10 de marzo de 2026). La publicación sobre la beta de Chrome 146 incluye trigger-scope en la misma versión. Trata toda la funcionalidad como una mejora progresiva.

El envoltorio @supports del ejemplo se encarga de ello. Los navegadores sin soporte omiten todo el bloque y renderizan el contenido de forma estática. Si quieres el mismo movimiento en otros navegadores, mantén tu cambio de clase con IntersectionObserver fuera de la feature query y ejecútalo solo cuando CSS.supports() devuelva false. Nuestro artículo sobre cómo reemplazar librerías de animación con APIs web nativas explica cómo construir ese fallback sin dependencias al estilo de AOS.

La query prefers-reduced-motion: no-preference hace que los usuarios que solicitan menos movimiento vean el contenido de inmediato. Para escalonar varios elementos, haz que su animation-trigger apunte a un mismo trigger compartido con distintos retrasos.

Conclusión

Las animaciones activadas por scroll resuelven el patrón de fade-in al hacer scroll: CSS decide cuándo empieza una animación ordinaria y el navegador se encarga de la observación. Empieza con el ejemplo anterior. Elige la acción (repetir, invertir o ejecutar una vez) que encaje con tu diseño, ajusta el rango para elementos altos o headers sticky, y elimina el código del observer en los navegadores compatibles manteniéndolo para el resto.

Preguntas frecuentes

¿Cuál es la diferencia entre las animaciones activadas por scroll y las scroll-state container queries?

Las scroll-state container queries aplican estilos mientras un contenedor se encuentra en un estado de scroll gestionado por el navegador. Las animaciones activadas por scroll inician una animación de keyframes cuando un elemento cruza un rango en un view timeline o un scroll timeline. Chrome 133 incorporó los estados stuck, snapped y scrollable, y Chrome 144 añadió scrolled, que registra la dirección de scroll más reciente. Usa una scroll-state query para un header sticky que cambia de estilo al quedar fijado, y un trigger para efectos de aparición.

¿Son las animaciones activadas por scroll más rápidas que una aparición con IntersectionObserver?

Las notas de la versión de Chrome indican que, al estar la interacción descrita en CSS, el navegador puede delegar el trabajo a un worker thread. Una aparición con IntersectionObserver ejecuta su callback y el cambio de clase en el hilo principal, por lo que una carga intensa de scripts puede retrasarla. Chrome presenta esta delegación como una ventaja de diseño, no como un resultado medido, así que analiza el rendimiento de tus propias páginas antes y después del cambio en lugar de dar por hecha una mejora.

¿Pueden los triggers de scroll controlar animaciones creadas con la Web Animations API?

Sí. Las notas de la versión de Chrome 146 indican que la funcionalidad incluye interfaces de JavaScript, de modo que los triggers pueden controlar animaciones creadas con la Web Animations API y no solo animaciones CSS. Los nombres de las interfaces están definidos en el Editor's Draft de CSS Animation Triggers Level 1, que todavía puede cambiar, así que consulta el borrador y la versión de Chrome a la que te diriges antes de depender de ellas.

¿Por qué mi elemento sigue oculto después de añadir un trigger de scroll?

El estado inicial oculto se aplica en cuanto la feature query coincide, por lo que el elemento permanece con opacity 0 hasta que se activa su trigger. Las causas más habituales son una discrepancia ortográfica entre el nombre de timeline-trigger y el de animation-trigger, un elemento animado fuera del subárbol definido por trigger-scope o un rango que el elemento nunca alcanza. Comprueba primero el nombre y después confirma que el elemento animado se encuentra dentro del elemento que define el ámbito.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.