Pasar al contenido principal

«Scrollytelling» usando scrollama.js, CSS y buenas prácticas

Desde hace unos años son bastante populares una serie de técnicas con un punto en común: la introducción de animaciones o interacciones ligadas al avance del usuario por la página (scroll) que muchas veces nos permiten «contar» una historia (scrollytelling).

En su formato más simple, esta técnica se puede utilizar para hacer «aparecer» ciertos elementos cuando alcanzan un punto fijo en la pantalla, dando así una sensación de mayor dinamismo y atrapando la atención del usuario. Es una técnica que podemos ver en cientos de sitios como, por ejemplo, la web de Opigno o este minisite de Netlify.

En este tutorial vamos a contar paso a paso cómo añadimos recientemente micro-animaciones a Metadrop.net y cómo puedes hacerlo en tus propios sitios usando nuestro nuevo módulo contribuído y la librería Scrollama.js.

Por qué Scrollama.js

Hasta hace unos años, las librerías de scroll tenían que escuchar los eventos de scroll o incluso llegaban a sustituir el scroll nativo por uno simulado (scrolljacking), lo cual provocaba grandes problemas de rendimiento, especialmente en móvil (scroll a saltos, interrumpido, etc.).

Por fortuna, hoy día existe la API de IntersectionObserver que permite operar de un modo mucho más preciso.

A la hora de abordar las animaciones en metadrop.net, optamos por Scrollama por dos grandes motivos:

  • Fue una de las primeras librerías en utilizar IntersectionObserver.
  • En lugar de incluir una librería entera de animaciones precocinadas y funcionalidades de todo tipo y pelaje, Scrollama se centra en exponer una API simple y minimalista que detecta la entrada/salida de elementos, dejando al desarrollador elegir cómo reaccionar.

En cierto sentido, Scrollama es el heredero de Waypoints.js, otra conocida librería cuyo espíritu también es proporcionar las herramientas y abstracciones necesarias para operar... y nada más.

Tras elegir la herramienta, desarrollamos y contribuimos un sencillo módulo para Drupal que nos permitiera agilizar el proceso de desarrollo. 

Este módulo simplemente expone una pequeña API basada en data-attributes que permite crear animaciones directamente con HTML y CSS, sin tener que escribir ni una línea de JavaScript. 


Manos a la obra: las animaciones

El modo más sencillo de animar un elemento consiste en añadir una clase cuando éste llega al punto indicado en pantalla y así poder delegar en el CSS todas las animaciones y cambios.

Frente a un enfoque basado en puro JavaScript, esto nos introduce una serie de beneficios:

  • La sintaxis de transition y keyframes de CSS es sencilla y potente.
  • Las animaciones se benefician de la GPU, podemos avisar al navegador mediante la propiedad will-change para mejorar el rendimiento y podemos respetar las elecciones del usuario mediante la media query prefers-reduced-motion.
  • Más fácil de mantener, escalar y modificar: basta con tocar una hoja de estilos CSS.

Pongamos que queremos animar nuestros elementos de dos modos: entrando desde la derecha o desde la izquierda. Una pequeña prueba de concepto sería la siguiente:

Configurar Scrollama

Tenemos nuestras animaciones: ahora nos falta hacer que sólo se ejecuten cuando el usuario hace scroll.

Para ello, vamos a cargar el módulo Scrollama del modo habitual. Si usamos composer, es tan fácil como escribir esto:

composer require drupal/scrollama

Vamos a la página de configuración y — por el momento — vamos a activarlo globalmente. También vamos a activar el modo «debug» para poder ver nuestro punto de entrada, el cual vamos a definir al 0.66, es decir, al 66% del tamaño de la pantalla desde el borde superior.

Si recargamos cualquier página veremos… nada. Esto se debe a que Scrollama solo se instancia si detecta al menos un elemento activo. ¿Y cómo se activa un elemento? Pues, como dijimos antes, mediante data-attributes.

Concretamente, mediante estos tres:

  • data-scroll-init - Es el único atributo obligatorio. Indica que el elemento HTML se animará, e indica también cuál es la clase o clases que recibirá al atravesar el punto de scroll.
  • data-scroll-exit- Alternativamente, se puede indicar también una clase a recibir en la salida del punto, es decir, cuando el límite inferior del elemento HTML supere también el punto definido en pantalla.
  • data-scroll-delay - Opcionalmente se puede definir un retardo expresado en segundos (“1” o “1s”). La clase CSS no se añadirá hasta pasado este tiempo.

Es decir, nuestros elementos HTML podrían acabar siendo algo tan sencillo como esto:

<div data-scroll-init=”enter-right”>
  <!-- Mi contenido… -->
</div>
<div data-scroll-init=”enter-left”>
  <!-- Mi contenido… -->
</div>

Ahora, nuestros elementos reciben una clase al atravesar el punto de scroll que podemos usar para introducir nuestras interacciones.


El estado inicial y cómo solucionarlo

Nos tocará reescribir un poco nuestro CSS para adaptarlo, ya que ahora tenemos el problema del estado inicial. En el ejemplo anterior hemos ocultado el elemento en el «from» de nuestro @keyframes, pero este estado no se aplicará hasta que no se añada la animación.

Nosotros queremos que el elemento HTML se encuentre inicialmente invisible para poder animar su aparición.

La solución más directa y sencilla es usar una clase cualquiera para identificar nuestro elemento y así poder inicializarlo como hidden.

<div class=“will-animate” data-scroll-init=”enter-right”>
  <!-- Mi contenido… -->
</div>
<div class=“will-animate” data-scroll-init=”enter-left”>
  <!-- Mi contenido… -->
</div>
<style>
  @keyframes enter-right {
    from {
      opacity: 0;
      transform: translateX(50%);
    }
    to {
      opacity: 1;
      transform: translateX(0%);
    }
  }
  .will-animate {
    visbiility: hidden
  }
  .will-animate.enter-right {
    /* Muestra el elemento... */
    visibility: visible;
    /* ...y lo anima */
    animation: enter-right 1s ease-out;
  }
</style>

Si no podemos o queremos usar una clase, no hay problema ya que podemos utilizar también selectores de atributos de CSS sobre el propio data-attribute utilizado para instanciar scrollama.

[data-scroll-init] {
  visibility: hidden;
}

Estamos asumiendo que nuestra estrategia para ocultar el elemento es mediante la propiedad visibility, pero también podríamos reproducir el estado inicial de la animación. Esto tiene el inconveniente de que nos obligará a definir cada animación por separado:

[data-scroll-init~=”enter-right”] {
   opacity: 0;
   transform: translateX(50%);
}
[data-scroll-init~=”enter-left”] {
   opacity: 0;
   transform: translateX(-50%);
}

Nótese que estamos usando ~=; este selector indica que el data-attribute debe contiene la cadena especificada entera y separada por espacios. Es mejor usar este y no otros como *=.

Puedes usar cualquier combinación, pero la opción basada en [data-attributes] y visibility:hidden es más genérica y más fácil de mantener. Es importante usar visibility y no display: none para que el elemento mantenga su tamaño y no produzcamos un reflow de la página que afecte al rendimiento. Si quieres saber la diferencia entre repaint y reflow, lee este artículo.

Con cualquiera de estas configuraciones, obtenemos el resultado deseado:

Usar transiciones

Este mismo código, dado que alterna entre dos estados, se podría haber expresado también usando transiciones:

[data-scroll-init~="enter-right"] {
  opacity: 0;
  transform: translateX(50%);
}
.enter-right {
  transition: opacity 0.5 ease-out, transform 0.5s ease-out;
}

Es importante notar que en este caso no nos podemos librar de declarar el estado inicial animación por animación. Es como si la primera regla hiciera el trabajo del from de la animación y la segunda ejerciera de to.


El estado final y cómo solucionarlo

Si hemos usado animaciones sobre alguna propiedad definida fuera de la declaración @keyframes, es posible que veamos cómo este valor se restablece después de la ejecución de la animación. Generalmente, esto producirá resultados no deseados.

Esto se debe a que, por defecto, las animaciones CSS después de ejecutarse vuelven a cualquier estado definido inicialmente fuera de la animación.

Por ejemplo, fíjate en esto:

@keyframes enter-right {
  from {
    opacity: 0;
    transform: translateX(50%);
  }
  to {
    opacity: 1;
    transform: translateX(0%);
    /* Animamos el background desde el valor original y no desde el "from" */
    background-color: red;
  }
}
.will-animate {
  visibility: hidden;
  /* Este es el color original */
  background-color: green;
}
.will-animate.enter-right {
  visibility: visible;
  animation: enter-right 1s ease-out;
}

Se producirá un efecto indeseado por el que, tras finalizar la animación, el valor de background-color se restablecerá al inicial de green. Date cuenta de que esto no ocurre con opacity o transform porque estos valores se definen dentro de la animación.

Para evitarlo, debemos añadir la propiedad animation-fill-mode: forwards; que indica que tras la animación el estado final debe permanecer.

.enter-right {
  animation: enter-right 1s ease-out;
  animation-fill-mode: forwards
}

El resultado se puede ver en este ejemplo: los valores de la izquierda no tienen animation-fill-mode: forwards, los de la derecha sí.

Toques finales y buenas prácticas

Antes de acabar, deberíamos tener en cuenta una serie de mejoras que harán de nuestro código más respetuoso con las preferencias del usuario y mejorarán su rendimiento.

Respetar las preferencias de usuario

Existen usuarios que, por diversos motivos, prefieren no tener animaciones, transiciones ni efectos de cualquier tipo en las páginas web. 

Desde hace unos años, los navegadores permiten al usuario especificar esta preferencia y exponen una media query llamada prefers-reduced-motion que nos permite reaccionar ante esta configuración.

Aplicarla a nivel global es tan fácil como escribir lo siguiente

@media (prefers-reduced-motion) {
  * {
    animation: none;
    transition: none;    
  }
} 

Aunque, en nuestro caso, tenemos que tener en cuenta que estamos comenzando con el elemento oculto por lo que deberemos mostrarlo también:

@media (prefers-reduced-motion) {
  * {
    animation: none;  
  }
  [data-scroll-init] {
    visibility: visible
  }
}

Esto se puede complicar rápidamente, así que podemos pensarlo también al revés y activar las animaciones si el usuario no tiene preferencia alguna:

@media screen and (prefers-reduced-motion: no-preference) { 
  /* …todo nuestro código iría aquí */
}


Eliminar la dependencia de JavaScript

Otro problema que hemos introducido y que es muy común es que, en ausencia de JavaScript, nuestra página será inutilizable puesto que tenemos varios elementos ocultos que no se llegarán a mostrar.

La solución aquí puede ser el viejo truco de añadir una clase .no-js al body que es cambiada por .js cuando se inicializa JavaScript, algo que en jQuery es tan simple como esto (puedes ver aqui una alternativa en puro JavaScript)

$(document).ready(function(){
  $(window).removeClass(‘no-js’).addClass('js');
});

Bastaría entonces con restringir nuestros selectores para que solo actúen cuando la página tiene JavaScript activo:

.js [data-scroll-init] {
  visibility: hidden
}
.js .enter-right {
  animation: enter-right 1s ease-out;
}


Mejorar el rendimiento usando will-change

Existe una propiedad reciente y bastante bien soportada que nos permite anunciar al navegador cuándo se prevé que un elemento reciba cambios de estado que afectan a su renderizado (posición, animaciones, color, etc.).

Esto permite al navegador pasar el manejo de dicho elemento a la GPU, en lugar del comportamiento por defecto por el cual es la CPU quien se encarga del renderizado hasta que aparece una animación, momento en el que se lo pasa a la GPU — y momento en el que este «salto» puede producir problemas de rendimiento o que se preciban movimientos bruscos. Si quieres saber más, este artículo de Sara Soueidan lo explica muy bien.

Usarlo es tan facil como indicar el nombre de la o las propiedades que «van a cambiar» (¡will change, literalmente!), por ejemplo:

[data-scroll-init] {
  will-change: transform, opacity;
}


El siguiente nivel

Como hemos podido ver, Scrollama.js nos permite fácilmente añádir puntos en la pantalla y «disparar» manualmente acciones cuando ciertos elementos los atraviesan.

En el tutorial nos hemos centrado en micro-animaciones, pero hoy en día este tipo de interacciones se están usando para construir impresionantes páginas web. Dos de las técnicas que más han sonado los últimos años son el parallax y el scrollytelling: el primero se refiere al efecto por el cual algunos elementos se desplazan con una velocidad diferente a la del scroll y el segundo, típico de formatos periodísiticos, hace referencia a piezas narrativas en la cual el contenido se va mostrando y desarrollando a medida que el usuario avanza. Un buen ejemplo de parallax es la web del refresco Cann y en el scrollytelling destacan periódicos como The Guardian.

De lo más pequeño a lo más grande, todo ellos tienen algo en común: se pueden orquestar utilizando scrollama por detrás para proporcionar la capacidad de reaccionar ante la entrada y salida de elementos en el scroll.