Een parallax-effect maken bij scrollen

Een parallax-effect bij scrollen is een visuele techniek waarbij verschillende contentlagen met verschillende snelheden bewegen terwijl de gebruiker omlaag scrollt op een pagina. Dit wekt de illusie van diepte, waarbij elementen op de voorgrond sneller lijken te bewegen dan de elementen op de achtergrond. Dit leidt tot een aantrekkelijke en meeslepende browsefunctionaliteit. Dit effect bereik je het beste met CSS Scroll-Driven Animations, waarmee je animaties kunt koppelen aan de scrollpositie van een container.

Hoe wordt dit geïmplementeerd

Zo maakt u een basisparallaxeffect:

  1. Maak een wrapper-element: Dit element groepeert alle lagen van het parallax-effect. Het is niet het scrollbare element, dus de overloop moet worden afgekapt. Geef het ook een height die overeenkomt met de hoogte van een van de lagen van het parallaxeffect.

    <div class="wrapper">
      …
    </div>
    
    .wrapper {
      overflow: clip;
      height: 100vh; /* Height of one of the layers of the parallax */
    }
    
  2. De lagen definiëren: Voeg in de wrapper de afzonderlijke lagen toe die met verschillende snelheden bewegen.

    <div class="wrapper">
      <div class="layer">LAYER 0</div>
      <div class="layer">LAYER 1</div>
      <div class="layer">LAYER 2</div>
      …
    </div>
    
  3. Voeg een vertaalanimatie toe: Definieer een CSS-animatie die de eigenschap transform van de lagen wijzigt. Voor een parallax-effect gebruikt u meestal translateY om de lagen verticaal te verplaatsen.

    @keyframes parallax {
      from {
        transform: translateY(700px);
      }
    }
    
  4. De view-timeline instellen: Als u de animatie aan de scrollpositie wilt koppelen, maakt u een view-timeline voor het wrapper-element en past u deze toe op de lagen.

    .wrapper {
      view-timeline: --wrapper;
    }
    
    .layer {
      animation: parallax linear both;
      animation-timeline: --wrapper;
    }
    
  5. De animaties spreiden: Als je de lagen met verschillende snelheden wilt laten bewegen, kun je een van de volgende 2 hoofdbenaderingen gebruiken: spreiden in de keyframes of de animation-range spreiden.

    Beide benaderingen kunnen hardgecodeerde waarden gebruiken of de sibling-index()/sibling-count()-implementatie. De vastgelegde waarden zijn het makkelijkst en ook nuttig als je maar een beperkt aantal lagen hebt. De implementatie van sibling-index()/sibling-count() is handig als je veel lagen hebt.

    • Spreiding in de sleutelframes:

      Met hardcoded waarden kun je een aangepaste eigenschap voor elke laag definiëren om de parallax-offset handmatig te beheren.

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

      Gebruik sibling-index() en laat de functie sibling-index() de index van een onderliggend element tussen de andere onderliggende elementen retourneren om het verspreide effect automatisch te berekenen.

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

      Met hardcoded waarden kunt u de grenzen van de animation-range voor elke laag afzonderlijk definiëren.

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

      Met sibling-index() en sibling-count() kun je het bereik wiskundig berekenen op basis van het totale aantal lagen (sibling-count()).

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

Voorbeeldcode

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

Je kunt ook de eigenschap animation-range gebruiken om een vergelijkbaar effect te bereiken:

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

Praktische tips

Als je scrollgestuurde animaties gebruikt, is het belangrijk dat je een aantal best practices volgt om een soepele en toegankelijke gebruikerservaring te bieden:

  • WEL functiedetectie opnemen: Niet alle browsers ondersteunen scrollgestuurde animaties. Gebruik @supports ((animation-timeline: view()) and (animation-range: entry)) om te controleren op ondersteuning en een fallback te bieden voor browsers die dit niet ondersteunen.
    • De (animation-range: entry)-controle MOET hier worden opgenomen om browsers met slechts gedeeltelijke ondersteuning weg te filteren.
    • Gebruik het scroll-timeline-polyfill-pakket NIET voor de fallback-strategie, omdat het niet alle functies bevat en veel bekende problemen heeft.
    • Als de animatie alleen als decoratief wordt beschouwd, kies je voor progressive enhancement en bied je GEEN fallback.
  • WEL: Respecteer gebruikersvoorkeuren. Sommige gebruikers geven de voorkeur aan minder beweging op het web. Gebruik de mediaquery prefers-reduced-motion om je animaties uit te zetten of te verminderen voor deze gebruikers.
  • WEL proberen alleen animaties te maken voor presterende CSS-property's: Voor de soepelste animaties maak je alleen animaties voor property's die kunnen worden verwerkt door de compositor-thread van de browser, zoals transform en opacity. Als je andere eigenschappen animeert, zoals width of height, kan dit leiden tot prestatieproblemen.
  • WEL de juiste declaratievolgorde gebruiken: Als je de verkorte eigenschap animation gebruikt, declareer je animation-timeline en animation-range erna om te voorkomen dat de verkorte eigenschap de tijdlijn reset.

Zo stel je de animation-range in:

  • WEL: Geef alle lagen dezelfde startoffset, bijvoorbeeld entry 25%
  • WEL: Geef alle lagen een andere eindoffset die sibling-count() en sibling-index() gebruikt om de offsets te verdelen, bijvoorbeeld exit calc(100% / sibling-count() * sibling-index()).

Browserondersteuning en fallback-strategieën

Daarom is meestal een reservestrategie vereist.

Voor browsers die geen scrollgestuurde animaties ondersteunen, kun je een fallback gebruiken om de visuele effecten opnieuw te maken. De fallbacks worden meestal gemaakt met een scroll-listener (voor ScrollTimeline-effecten) of de IntersectionObserver API (voor ViewTimeline-effecten).

In browsers met ingebouwde ondersteuning voor scrollgestuurde animaties gebruik je ALTIJD de native CSS-implementatie, omdat die beter presteert.

Niet elk effect kan opnieuw worden gemaakt met de reserveaanpak.

Specifiek voor deze use case past het volgende script de fallback toe voor browsers die geen scrollgestuurde animaties ondersteunen. De functie gebruikt een IntersectionObserver om de zichtbaarheid van het element .wrapper bij te houden en updatet de property transform van de lagen op basis van de scrollpositie.

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