El efecto de paralaje en el desplazamiento es una técnica visual en la que diferentes capas de contenido se mueven a distintas velocidades a medida que el usuario se desplaza hacia abajo en una página. Esto crea una ilusión de profundidad, en la que los elementos en primer plano parecen moverse más rápido que los del fondo, lo que genera una experiencia de navegación atractiva y envolvente. Este efecto se logra mejor con las animaciones basadas en el desplazamiento de CSS, que te permiten vincular animaciones a la posición de desplazamiento de un contenedor.
Cómo implementarlo
Sigue estos pasos para crear un efecto de paralaje básico:
Crea un elemento contenedor: Este elemento simplemente agrupa todas las capas del efecto de paralaje. No es el elemento desplazable, por lo que se debe recortar su desbordamiento. También asígnale un valor de
heightque coincida con la altura de una de las capas del efecto de paralaje.<div class="wrapper"> … </div>.wrapper { overflow: clip; height: 100vh; /* Height of one of the layers of the parallax */ }Declara las capas: Dentro del contenedor, agrega las capas individuales que se moverán a diferentes velocidades.
<div class="wrapper"> <div class="layer">LAYER 0</div> <div class="layer">LAYER 1</div> <div class="layer">LAYER 2</div> … </div>Agrega una animación de traducción: Define una animación de CSS que cambie la propiedad
transformde las capas. Para lograr un efecto de paralaje, por lo general, usarástranslateYpara mover las capas de forma vertical.@keyframes parallax { from { transform: translateY(700px); } }Configura el
view-timeline: Para vincular la animación a la posición de desplazamiento, crea unview-timelineen el elemento wrapper y, luego, aplícalo a las capas..wrapper { view-timeline: --wrapper; } .layer { animation: parallax linear both; animation-timeline: --wrapper; }Escalona las animaciones: Para que las capas se muevan a diferentes velocidades, puedes usar uno de los dos enfoques principales: escalonar los fotogramas clave o escalonar el
animation-range.Ambos enfoques pueden usar valores codificados o la implementación de
sibling-index()/sibling-count(). Los valores codificados son los más fáciles y también son útiles cuando solo se tiene una cantidad limitada de capas. La implementación desibling-index()/sibling-count()es útil cuando tienes muchas capas.Escalonamiento en los fotogramas clave:
Con los valores codificados, puedes definir una propiedad personalizada para cada capa y controlar manualmente su desplazamiento de paralaje.
.layer:nth-child(1) { --offset: 100px; } .layer:nth-child(2) { --offset: 200px; } .layer:nth-child(3) { --offset: 300px; } @keyframes parallax { from { transform: translateY(var(--offset)); } }Con
sibling-index(), permite que la funciónsibling-index()devuelva el índice de un elemento secundario entre sus elementos hermanos para calcular automáticamente el efecto escalonado.@keyframes parallax { from { transform: translateY(calc(100px * sibling-index())); } }Escalonamiento de
animation-range:Con los valores codificados, puedes definir de forma explícita los límites de
animation-rangeen cada capa de manera individual..layer:nth-child(1) { animation-range: entry 25% exit 50%; } .layer:nth-child(2) { animation-range: entry 25% exit 75%; } .layer:nth-child(3) { animation-range: entry 25% exit 100%; }Con
sibling-index()ysibling-count(), puedes calcular el rango de forma matemática según la cantidad total de capas (sibling-count())..layer { animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index()); }
Ejemplo de código
@keyframes parallax {
from {
transform: translateY(calc(100px * sibling-index()));
}
}
.wrapper {
view-timeline: --wrapper;
}
.layer {
animation: parallax linear both;
animation-timeline: --wrapper;
}
@media (prefers-reduced-motion: reduce) {
.layer {
animation: none;
}
}
Como alternativa, puedes usar la propiedad animation-range para lograr un efecto similar:
@keyframes parallax {
from {
transform: translateY(700px);
}
}
.wrapper {
view-timeline: --wrapper;
}
.layer {
animation: parallax linear both;
animation-timeline: --wrapper;
animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index());
}
@media (prefers-reduced-motion: reduce) {
.layer {
animation: none;
}
}
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: view()) and (animation-range: entry))para verificar la compatibilidad y proporcionar un resguardo para los navegadores que no lo admiten.- La verificación de
(animation-range: entry)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.
En cuanto a la configuración de animation-range, haz lo siguiente:
- SÍ asignes el mismo desplazamiento de inicio a todas las capas, p. ej.,
entry 25%. - SÍ asignes a todas las capas un desplazamiento final diferente que use
sibling-count()ysibling-index()para distribuir los desplazamientos, p.ej.,exit calc(100% / sibling-count() * sibling-index()).
Compatibilidad con navegadores y estrategias de resguardo
Por lo tanto, suele ser necesaria una estrategia 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 del elemento .wrapper y actualiza la propiedad transform de las capas según la posición de desplazamiento.
// Fallback for browsers that don't support scroll-driven animations
if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
const wrapper = document.querySelector('.wrapper');
const layers = document.querySelectorAll('.layer');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
window.addEventListener('scroll', onScroll);
} else {
window.removeEventListener('scroll', onScroll);
}
});
}, { threshold: 0 });
observer.observe(wrapper);
function onScroll() {
const scrollY = window.scrollY;
const wrapperRect = wrapper.getBoundingClientRect();
const wrapperTop = wrapperRect.top + scrollY;
const wrapperHeight = wrapperRect.height;
const windowHeight = window.innerHeight;
if (scrollY >= wrapperTop - windowHeight && scrollY <= wrapperTop + wrapperHeight) {
const scrollPercent = (scrollY - (wrapperTop - windowHeight)) / (wrapperHeight + windowHeight);
layers.forEach((layer, index) => {
// This matches the effect as defined in the CSS example above.
// Customize this further if needed.
const initialTranslateY = 100 * index;
const translateY = initialTranslateY * (1 - scrollPercent);
layer.style.transform = `translateY(${translateY}px)`;
});
}
}
// Trigger onScroll once to set initial positions
onScroll();
}