Créer un effet parallaxe au défilement

L'effet de parallaxe au défilement est une technique visuelle qui consiste à faire défiler différentes couches de contenu à des vitesses variables lorsque l'utilisateur fait défiler une page vers le bas. Cela crée une illusion de profondeur, les éléments au premier plan semblant se déplacer plus rapidement que ceux en arrière-plan, ce qui offre une expérience de navigation attrayante et immersive. Pour obtenir cet effet, il est préférable d'utiliser des animations CSS liées au défilement, qui vous permettent d'associer des animations à la position de défilement d'un conteneur.

Mise en œuvre

Voici comment créer un effet parallaxe de base :

  1. Créer un élément wrapper : cet élément regroupe simplement toutes les couches de l'effet parallaxe. Il ne s'agit pas de l'élément à faire défiler. Son dépassement doit donc être tronqué. Attribuez-lui également un height correspondant à la hauteur de l'une des couches de l'effet de parallaxe.

    <div class="wrapper">
      …
    </div>
    
    .wrapper {
      overflow: clip;
      height: 100vh; /* Height of one of the layers of the parallax */
    }
    
  2. Déclarez les couches : dans le wrapper, ajoutez les couches individuelles qui se déplaceront à des vitesses différentes.

    <div class="wrapper">
      <div class="layer">LAYER 0</div>
      <div class="layer">LAYER 1</div>
      <div class="layer">LAYER 2</div>
      …
    </div>
    
  3. Ajouter une animation de translation : définissez une animation CSS qui modifie la propriété transform des calques. Pour un effet de parallaxe, vous utiliserez généralement translateY pour déplacer les calques verticalement.

    @keyframes parallax {
      from {
        transform: translateY(700px);
      }
    }
    
  4. Configurer view-timeline : pour lier l'animation à la position de défilement, créez un view-timeline sur l'élément wrapper, puis appliquez-le aux calques.

    .wrapper {
      view-timeline: --wrapper;
    }
    
    .layer {
      animation: parallax linear both;
      animation-timeline: --wrapper;
    }
    
  5. Échelonner les animations : pour que les calques se déplacent à des vitesses différentes, vous pouvez utiliser l'une des deux approches principales : échelonner les images clés ou échelonner les animation-range.

    Ces deux approches peuvent utiliser des valeurs codées en dur ou l'implémentation sibling-index()/sibling-count(). Les valeurs codées en dur sont les plus simples et sont également utiles lorsque vous n'avez qu'un nombre limité de couches. L'implémentation sibling-index()/sibling-count() est pratique lorsque vous avez de nombreuses couches.

    • Décalage dans les images clés :

      À l'aide de valeurs codées en dur, vous pouvez définir une propriété personnalisée pour chaque calque afin de contrôler manuellement son décalage de parallaxe.

      .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));
        }
      }
      

      En utilisant sibling-index(), laissez la fonction sibling-index() renvoyer l'index d'un élément enfant parmi ses éléments frères pour calculer automatiquement l'effet d'échelonnement.

      @keyframes parallax {
        from {
          transform: translateY(calc(100px * sibling-index()));
        }
      }
      
    • Échelonner les animation-range :

      En utilisant des valeurs codées en dur, vous pouvez définir explicitement les limites de animation-range sur chaque couche individuellement.

      .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%; }
      

      À l'aide de sibling-index() et sibling-count(), vous pouvez calculer la plage mathématiquement en fonction du nombre total de couches (sibling-count()).

      .layer {
        animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index());
      }
      

Exemple de code

@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;
  }
}

Vous pouvez également utiliser la propriété animation-range pour obtenir un effet similaire :

@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;
  }
}

Bonnes pratiques

Lorsque vous utilisez des animations basées sur le défilement, il est important de suivre quelques bonnes pratiques pour garantir une expérience fluide et accessible :

  • À FAIRE : incluez la détection de fonctionnalités. Tous les navigateurs ne sont pas compatibles avec les animations déclenchées par le défilement. Utilisez @supports ((animation-timeline: view()) and (animation-range: entry)) pour vérifier la compatibilité et fournir une solution de remplacement pour les navigateurs qui ne sont pas compatibles.
    • La vérification (animation-range: entry) DOIT être incluse ici pour filtrer les navigateurs qui ne sont que partiellement compatibles.
    • N'UTILISEZ PAS le package scroll-timeline-polyfill pour la stratégie de remplacement, car il n'est pas complet et présente de nombreux problèmes connus.
    • Si l'animation n'est considérée que comme décorative, optez pour l'amélioration progressive et NE FOURNISSEZ PAS de solution de remplacement.
  • À FAIRE : Respectez les préférences des utilisateurs. Certains préfèrent avoir moins de mouvements sur le Web. Utilisez la requête média prefers-reduced-motion pour désactiver ou réduire vos animations pour ces utilisateurs.
  • À FAIRE : Essayez de n'animer que les propriétés CSS performantes. Pour des animations plus fluides, n'animez que les propriétés pouvant être gérées par le thread du compositeur du navigateur, telles que transform et opacity. L'animation d'autres propriétés telles que width ou height peut entraîner des problèmes de performances.
  • À FAIRE : Utilisez l'ordre de déclaration correct. Lorsque vous utilisez la propriété abrégée animation, déclarez animation-timeline et animation-range après pour éviter que la propriété abrégée ne réinitialise la chronologie.

Pour définir le animation-range :

  • À FAIRE : Donnez à toutes les couches le même décalage de début, par exemple entry 25%.
  • À FAIRE : attribuez à chaque calque un décalage de fin différent qui utilise sibling-count() et sibling-index() pour répartir les décalages, par exemple exit calc(100% / sibling-count() * sibling-index()).

Compatibilité des navigateurs et stratégies de remplacement

Par conséquent, une stratégie de remplacement est généralement requise.

Pour les navigateurs qui ne sont pas compatibles avec les animations déclenchées par le défilement, vous pouvez utiliser une solution de remplacement pour recréer les effets visuels. Les solutions de remplacement sont généralement créées avec un écouteur de défilement (pour les effets ScrollTimeline) ou l'API IntersectionObserver (pour les effets ViewTimeline).

Dans les navigateurs compatibles de manière native avec les animations liées au défilement, utilisez TOUJOURS l'implémentation CSS native, car elle est plus performante.

Notez que tous les effets ne peuvent pas être recréés à l'aide de l'approche de remplacement.

Pour ce cas d'utilisation spécifique, le script suivant applique la solution de remplacement pour les navigateurs qui ne sont pas compatibles avec les animations déclenchées par le défilement. Il utilise un IntersectionObserver pour suivre la visibilité de l'élément .wrapper et met à jour la propriété transform des calques en fonction de la position de défilement.

// 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();
}