Crea un effetto di parallasse allo scorrimento

L'effetto di parallasse durante lo scorrimento è una tecnica visiva in cui i diversi livelli di contenuti si muovono a velocità diverse mentre l'utente scorre una pagina. In questo modo si crea un'illusione di profondità, con gli elementi in primo piano che sembrano muoversi più velocemente di quelli sullo sfondo, offrendo un'esperienza di navigazione coinvolgente e immersiva. Questo effetto si ottiene al meglio utilizzando le animazioni basate sullo scorrimento CSS, che consentono di collegare le animazioni alla posizione di scorrimento di un contenitore.

Come implementare

Ecco come creare un effetto di parallasse di base:

  1. Crea un elemento wrapper: questo elemento raggruppa semplicemente tutti i livelli dell'effetto di parallasse. Non è l'elemento scorrevole, quindi il suo overflow deve essere ritagliato. Assegna anche un valore height che corrisponda all'altezza di uno dei livelli dell'effetto di parallasse.

    <div class="wrapper">
      …
    </div>
    
    .wrapper {
      overflow: clip;
      height: 100vh; /* Height of one of the layers of the parallax */
    }
    
  2. Dichiara i livelli: all'interno del wrapper, aggiungi i singoli livelli che si muoveranno a velocità diverse.

    <div class="wrapper">
      <div class="layer">LAYER 0</div>
      <div class="layer">LAYER 1</div>
      <div class="layer">LAYER 2</div>
      …
    </div>
    
  3. Aggiungi un'animazione di traslazione: definisci un'animazione CSS che modifica la proprietà transform dei livelli. Per un effetto di parallasse, in genere utilizzerai translateY per spostare i livelli verticalmente.

    @keyframes parallax {
      from {
        transform: translateY(700px);
      }
    }
    
  4. Configura view-timeline: per collegare l'animazione alla posizione di scorrimento, crea un view-timeline sull'elemento wrapper e applicalo ai livelli.

    .wrapper {
      view-timeline: --wrapper;
    }
    
    .layer {
      animation: parallax linear both;
      animation-timeline: --wrapper;
    }
    
  5. Scagliona le animazioni: per far muovere i livelli a velocità diverse, puoi utilizzare uno dei due approcci principali: scaglionamento nei keyframe o scaglionamento di animation-range.

    Entrambi gli approcci possono utilizzare valori impostati come hardcoded o l'implementazione sibling-index()/sibling-count(). I valori impostati come hardcoded sono i più semplici e sono utili anche quando si ha un numero limitato di livelli. L'implementazione sibling-index()/sibling-count() è utile quando hai molti livelli.

    • Scaglionamento dei fotogrammi chiave:

      Utilizzando valori impostati come hardcoded, puoi definire una proprietà personalizzata per ogni livello per controllare manualmente l'offset di parallasse.

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

      Utilizzando sibling-index(), consenti alla funzione sibling-index() di restituire l'indice di un elemento secondario tra i relativi elementi di pari livello per calcolare automaticamente l'effetto di scaglionamento.

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

      Utilizzando valori impostati come hardcoded, puoi definire esplicitamente i limiti di animation-range su ogni livello singolarmente.

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

      Utilizzando sibling-index() e sibling-count(), puoi calcolare l'intervallo matematicamente in base al numero totale di livelli (sibling-count()).

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

Esempio di codice

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

In alternativa, puoi utilizzare la proprietà animation-range per ottenere un effetto simile:

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

Best practice

Quando utilizzi le animazioni di scorrimento, è importante seguire alcune best practice per garantire un'esperienza fluida e accessibile:

  • SÌ: includi il rilevamento delle funzionalità. Non tutti i browser supportano le animazioni basate sullo scorrimento. Utilizza @supports ((animation-timeline: view()) and (animation-range: entry)) per verificare il supporto e fornire un'alternativa per i browser che non lo supportano.
    • Il controllo (animation-range: entry) DEVE essere incluso qui per filtrare i browser con supporto solo parziale.
    • NON utilizzare il pacchetto scroll-timeline-polyfill per la strategia di riserva, in quanto non è completo di funzionalità e presenta molti problemi noti.
    • Se l'animazione è considerata solo decorativa, opta per il potenziamento progressivo e NON fornire un fallback.
  • SÌ: rispetta le preferenze degli utenti. Alcuni utenti preferiscono un movimento ridotto sul web. Utilizza la query supporti prefers-reduced-motion per disattivare o ridurre le animazioni per questi utenti.
  • SÌ: prova ad animare solo le proprietà CSS con prestazioni elevate. Per ottenere animazioni più fluide, limita l'animazione alle proprietà che possono essere gestite dal thread del compositore del browser, come transform e opacity. L'animazione di altre proprietà come width o height può causare problemi di prestazioni.
  • SÌ: utilizza l'ordine di dichiarazione corretto. Quando utilizzi la proprietà abbreviata animation, dichiara animation-timeline e animation-range dopo per evitare che la proprietà abbreviata reimposti la sequenza temporale.

Per impostare animation-range:

  • SÌ: assegna a tutti i livelli lo stesso offset iniziale, ad esempio entry 25%
  • SÌ: assegna a tutti i livelli un offset finale diverso che utilizzi sibling-count() e sibling-index() per distribuire gli offset, ad esempio exit calc(100% / sibling-count() * sibling-index()).

Supporto del browser e strategie di riserva

Pertanto, in genere è necessaria una strategia di riserva.

Per i browser che non supportano le animazioni basate sullo scorrimento, puoi utilizzare un comportamento di riserva per ricreare gli effetti visivi. I fallback vengono in genere creati con un listener di scorrimento (per gli effetti ScrollTimeline) o con l'API IntersectionObserver (per gli effetti ViewTimeline).

Nei browser con supporto integrato per le animazioni basate sullo scorrimento, utilizza SEMPRE l'implementazione CSS nativa, in quanto è più efficiente.

Tieni presente che non tutti gli effetti possono essere ricreati utilizzando l'approccio dei fallback.

Per questo caso d'uso specifico, lo script seguente applica il comportamento di riserva per i browser che non supportano le animazioni basate sullo scorrimento. Utilizza un IntersectionObserver per monitorare la visibilità dell'elemento .wrapper e aggiorna la proprietà transform dei livelli in base alla posizione di scorrimento.

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