Scrollytelling

Scrollytelling is een populaire techniek om boeiende en meeslepende webervaringen te maken. Hierbij worden elementen op een pagina geanimeerd terwijl de gebruiker scrollt, waardoor een verhaal wordt verteld of de gebruiker door een verhaal wordt geleid. Met CSS-animaties op basis van scrollen kun je deze effecten rechtstreeks in CSS maken, zonder dat je JavaScript nodig hebt. De animaties worden bepaald door de scrollpositie, niet door een tijdklok. Zo lopen ze altijd synchroon met de scrollbeweging van de gebruiker.

Hoe wordt dit geïmplementeerd

Als je een scrollytelling-beleving wilt maken, heb je 2 sets elementen nodig: een om de scrollpositie bij te houden en een andere om te animeren.

Definieer eerst een benoemde view-timeline voor de elementen die u wilt bijhouden. Deze fungeren als de aansturing voor je animaties.

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

Pas daarna animaties toe op de elementen die u wilt animeren en koppel ze aan de tijdlijnen die u net heeft gemaakt met de property 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; }
}

De animation-timeline moet in hetzelfde bereik staan als de benoemde tijdlijnen om ernaar te kunnen verwijzen. Je kunt de property timeline-scope gebruiken voor een gemeenschappelijke voorouder om de tijdlijnen beschikbaar te maken voor alle elementen die ze nodig hebben. Het element :root is hiervoor vaak een goede keuze.

html {
  timeline-scope: --tl-1, --tl-2, --tl-3, --tl-4, --tl-5;
}

Tot slot kunt u de property animation-range gebruiken om het exacte bereik van de tijdlijn aan te geven waarin de animatie moet worden uitgevoerd. Zo heeft u gedetailleerde controle over wanneer de animaties worden geactiveerd en hoe ze verlopen.

#animated section {
  animation-range: entry 25% cover 50%, exit 50% exit 75%;
}

Voorbeeldcode

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

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: scroll()) and (animation-range: 0% 100%)) om te controleren op ondersteuning en een fallback te bieden voor browsers die dit niet ondersteunen.
    • De (animation-range: 0% 100%)-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.

Als je de eigenschap view-timeline gebruikt om een scrollgestuurde animatie te maken:

  • WEL een CSS <dashed-ident> gebruiken voor de naam.
  • OPTIONEEL Wees expliciet over de as die moet worden bijgehouden: Als je niet de standaardas block target (zoals in een horizontale scroller), wees dan expliciet over welke as moet worden bijgehouden met view-timeline-axis.
  • Zorg dat het bereik van de zoekopdracht werkt: Als het element dat de view-timeline declareert, geen voorouder van de platte structuur van het geanimeerde element is, verhoog je de zichtbaarheid van de naam van de view-timeline door timeline-scope te gebruiken voor een gedeelde voorouder.

Reservestrategieën

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. Het gebruikt een IntersectionObserver om de zichtbaarheid van elk #tracked section-element bij te houden en updatet de transform-property van de bijbehorende #animated section.

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

En de bijbehorende CSS:

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

Deze fallback biedt een nauwkeurigere, scrollgestuurde animatie voor browsers die de systeemeigen CSS-functie niet ondersteunen. Zo krijgen alle gebruikers een consistentere functionaliteit. Door een reeks drempelwaarden voor de IntersectionObserver te gebruiken, kunnen we de scrollpositie nauwkeuriger bijhouden en een vloeiendere animatie maken.