El scrollytelling es una técnica popular que se usa para crear experiencias web atractivas y envolventes. Implica animar elementos en una página a medida que el usuario se desplaza, lo que permite contar una historia o guiar al usuario a través de una narrativa de manera eficaz. Con las animaciones basadas en el desplazamiento de CSS, puedes crear estos efectos directamente en CSS, sin necesidad de depender de JavaScript. Las animaciones se controlan con la posición de desplazamiento, no con un reloj basado en el tiempo, lo que garantiza que siempre estén sincronizadas con el desplazamiento del usuario.
Cómo implementarlo
Para crear una experiencia de scrollytelling, necesitas dos conjuntos de elementos: uno para hacer un seguimiento de la posición de desplazamiento y otro para animar.
Primero, define un view-timeline con nombre en los elementos de los que quieras hacer un seguimiento. Estos actuarán como los controladores de tus animaciones.
#tracked {
section:nth-child(1){ view-timeline: --tl-1 block; }
section:nth-child(2){ view-timeline: --tl-2 block; }
section:nth-child(3){ view-timeline: --tl-3 block; }
section:nth-child(4){ view-timeline: --tl-4 block; }
section:nth-child(5){ view-timeline: --tl-5 block; }
}
A continuación, aplica animaciones a los elementos que quieras animar y vincúlalos a los cronogramas que acabas de crear con la propiedad animation-timeline.
#animated {
section {
animation: animate-in auto linear both, animate-out auto linear forwards;
animation-range: entry 25% cover 50%, exit 50% exit 75%;
}
section:nth-child(1){ animation-timeline: --tl-1; }
section:nth-child(2){ animation-timeline: --tl-2; }
section:nth-child(3){ animation-timeline: --tl-3; }
section:nth-child(4){ animation-timeline: --tl-4; }
section:nth-child(5){ animation-timeline: --tl-5; }
}
Para que el animation-timeline pueda hacer referencia a las líneas de tiempo con nombre, deben estar en el mismo alcance. Puedes usar la propiedad timeline-scope en un ancestro común para que las líneas de tiempo estén disponibles para todos los elementos que las necesiten. El elemento :root suele ser una buena opción para esto.
html {
timeline-scope: --tl-1, --tl-2, --tl-3, --tl-4, --tl-5;
}
Por último, puedes usar la propiedad animation-range para especificar el intervalo exacto de la línea de tiempo durante el cual se debe ejecutar la animación. Esto te brinda un control detallado sobre cuándo se activan las animaciones y cómo progresan.
#animated section {
animation-range: entry 25% cover 50%, exit 50% exit 75%;
}
Ejemplo de código
html {
timeline-scope: --tl-1, --tl-2, --tl-3, --tl-4, --tl-5;
}
#tracked {
section:nth-child(1){ view-timeline: --tl-1 block; }
section:nth-child(2){ view-timeline: --tl-2 block; }
section:nth-child(3){ view-timeline: --tl-3 block; }
section:nth-child(4){ view-timeline: --tl-4 block; }
section:nth-child(5){ view-timeline: --tl-5 block; }
}
@keyframes animate-in {
from { scale: 0.5; opacity: 0; transform: rotateY(-180deg); }
to { transform: rotateY(0deg); }
}
@keyframes animate-out {
to { translate: 100% 0; opacity: 0; }
}
#animated {
section {
animation: animate-in auto linear both, animate-out auto linear forwards;
animation-range: entry 25% cover 50%, exit 50% exit 75%;
backface-visibility: hidden;
}
section:nth-child(1){ animation-timeline: --tl-1; }
section:nth-child(2){ animation-timeline: --tl-2; }
section:nth-child(3){ animation-timeline: --tl-3; }
section:nth-child(4){ animation-timeline: --tl-4; }
section:nth-child(5){ animation-timeline: --tl-5; }
}
/* MANDATORY Copy-Paste Safety: Disable continuous storytelling motion for sensitive users */
@media (prefers-reduced-motion: reduce) {
#animated section {
animation: none !important;
opacity: 1 !important;
transform: none !important;
}
}
Prácticas recomendadas
Cuando uses animaciones controladas por el desplazamiento, es importante que sigas algunas prácticas recomendadas para garantizar una experiencia fluida y accesible:
- SÍ incluye la detección de funciones: No todos los navegadores admiten animaciones controladas por el desplazamiento. Usa
@supports ((animation-timeline: scroll()) and (animation-range: 0% 100%))para verificar la compatibilidad y proporcionar un resguardo para los navegadores que no lo admiten.- La verificación de
(animation-range: 0% 100%)DEBE incluirse aquí para filtrar los navegadores que solo tienen compatibilidad parcial. - NO uses el paquete
scroll-timeline-polyfillpara la estrategia de resguardo, ya que no tiene todas las funciones y tiene muchos problemas conocidos. - Si la animación solo se considera decorativa, opta por la mejora progresiva y NO proporciones un resguardo.
- La verificación de
- SÍ respeta las preferencias del usuario: Algunos usuarios prefieren tener menos movimiento en la Web. Usa la consulta de medios
prefers-reduced-motionpara inhabilitar o reducir las animaciones para estos usuarios. - SÍ intenta animar solo las propiedades de CSS que tengan un buen rendimiento: Para obtener las animaciones más fluidas, limítate a animar las propiedades que pueda controlar el subproceso del compositor del navegador, como
transformyopacity. Animar otras propiedades, comowidthoheight, puede provocar problemas de rendimiento. - SÍ uses el orden de declaración correcto: Cuando uses la propiedad abreviada
animation, declaraanimation-timelineyanimation-rangedespués de ella para evitar que la abreviatura restablezca la línea de tiempo.
Cuando uses la propiedad view-timeline para crear una animación controlada por el desplazamiento, ten en cuenta lo siguiente:
- SÍ uses un
<dashed-ident>de CSS para el nombre. - OPCIONAL Sé explícito sobre el eje que se debe seguir: Cuando no se oriente al eje
blockpredeterminado (como en un desplazador horizontal), sé explícito sobre qué eje se debe seguir conview-timeline-axis. - SÍ asegúrate de que el alcance de la búsqueda funcione: Cuando el elemento que declara el
view-timelineno es un ancestro de árbol plano del elemento animado, eleva la visibilidad del nombre delview-timelinecontimeline-scopeen un ancestro compartido.
Estrategias de resguardo
En el caso de los navegadores que no admiten animaciones basadas en el desplazamiento, puedes usar un resguardo para recrear los efectos visuales. Por lo general, las alternativas se crean con un objeto de escucha de desplazamiento (para los efectos de ScrollTimeline) o la API de IntersectionObserver (para los efectos de ViewTimeline).
En los navegadores con compatibilidad integrada para animaciones basadas en el desplazamiento, usa SIEMPRE la implementación nativa de CSS, ya que tiene un mejor rendimiento.
Ten en cuenta que no todos los efectos se pueden recrear con el enfoque de resguardos.
Específicamente para este caso de uso, la siguiente secuencia de comandos aplica el resguardo para los navegadores que no admiten animaciones controladas por el desplazamiento. Usa un IntersectionObserver para hacer un seguimiento de la visibilidad de cada elemento #tracked section y actualiza la propiedad transform del #animated section correspondiente según corresponda.
const animatedSections = document.querySelectorAll('#animated section');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const sectionIndex = Array.from(document.querySelectorAll('#tracked section')).indexOf(entry.target);
if (sectionIndex !== -1) {
const animatedSection = animatedSections[sectionIndex];
const ratio = entry.intersectionRatio;
// Animate-in
animatedSection.style.opacity = ratio;
animatedSection.style.transform = `scale(${0.5 + ratio * 0.5}) rotateY(${-180 + ratio * 180}deg)`;
// Animate-out
if (ratio < 0.5) {
animatedSection.style.translate = `${(0.5 - ratio) * 2 * 100}% 0`;
} else {
animatedSection.style.translate = '0 0';
}
}
});
}, { threshold: Array.from({length: 101}, (_, i) => i / 100) });
document.querySelectorAll('#tracked section').forEach(section => {
observer.observe(section);
});
Y el CSS que lo acompaña:
#animated section {
opacity: 0;
transform: scale(0.5) rotateY(-180deg);
backface-visibility: hidden;
}
/* MANDATORY Copy-Paste Safety: Ensure content remains fully visible and legible for assistive technologies or users with motion sensitivities */
@media (prefers-reduced-motion: reduce) {
#animated section {
opacity: 1 !important;
transform: none !important;
translate: 0 0 !important;
}
}
Este resguardo proporciona una animación basada en el desplazamiento más precisa para los navegadores que no admiten la función nativa de CSS, lo que garantiza una experiencia más coherente para todos los usuarios. Si usamos una serie de umbrales para el IntersectionObserver, podemos hacer un seguimiento de la posición de desplazamiento con más precisión y crear una animación más fluida.