Zum Inhalt springen

Scroll-gesteuerte CSS-Animationen ohne JavaScript

Praxisleitfaden für performante scroll-gesteuerte Animationen mit nativen CSS-Scroll-Timelines, View Transitions und der Eigenschaft animation-timeline.

5 Min. Lesezeit
Flüssige scroll-gesteuerte Animationseffekte auf einer modernen Webseite

Das Ende der JavaScript-gesteuerten Scroll-Animationen

Ein Jahrzehnt lang erforderten Scroll-Animationen JavaScript. Man lauschte auf Scroll-Events, berechnete Positionen und aktualisierte Styles imperativ. Bibliotheken wie GSAP ScrollTrigger, Framer Motion und Intersection Observer machten das handhabbar, aber der grundlegende Ansatz — JavaScript, das den Main Thread kapert, um visuelle Effekte anzutreiben — war immer ein Performance-Kompromiss.

Scroll-gesteuerte CSS-Animationen ändern das vollständig. Die Eigenschaft animation-timeline verbindet CSS-Animationen direkt mit dem Scroll-Fortschritt. Der Browser übernimmt die Berechnungen auf dem Compositor-Thread und liefert Scroll-Effekte mit 60 fps ohne eine einzige Zeile JavaScript. Das ist kein Polyfill und kein Hack — es ist eine W3C-Spezifikation mit wachsender Browserunterstützung.

Scroll-Fortschrittsanimationen: Die Grundlagen

Eine scroll-gekoppelte Animation bildet den Animationsfortschritt eines Elements auf die Scroll-Position eines Containers ab. Statt über die Zeit (Sekunden) läuft die Animation über den Raum (Scroll-Distanz).

csscss
/* ❌ Old approach: JavaScript scroll listener */
/* 
  window.addEventListener('scroll', () => {
    const progress = window.scrollY / document.body.scrollHeight;
    element.style.transform = `scaleX(${progress})`;
  });
  // Runs on main thread, causes jank, needs throttling
*/
csscss
/* ✅ New approach: Pure CSS scroll animation */
.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 4px;
  background: linear-gradient(90deg, #3b82f6, #8b5cf6);
  transform-origin: left;
  animation: grow-progress linear both;
  animation-timeline: scroll();
}
 
@keyframes grow-progress {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

Die Deklaration animation-timeline: scroll() ersetzt den zeitbasierten Fortschritt durch einen scrollbasierten. Der Fill-Modus both sorgt dafür, dass der Animationszustand an beiden Enden erhalten bleibt. Kein JavaScript, kein Scroll-Listener, kein Ruckeln.

Benannte Scroll-Timelines für verschachtelte Container

Die Funktion scroll() verwendet standardmäßig den nächsten scrollbaren Vorfahren. Für mehr Kontrolle erlauben benannte Scroll-Timelines, Animationen an bestimmte scrollbare Elemente zu binden.

csscss
.scroll-container {
  overflow-y: auto;
  height: 600px;
  scroll-timeline-name: --container-scroll;
  scroll-timeline-axis: block;
}
 
.parallax-background {
  animation: parallax-shift linear both;
  animation-timeline: --container-scroll;
}
 
@keyframes parallax-shift {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(-200px);
  }
}
 
.fade-header {
  animation: header-fade linear both;
  animation-timeline: --container-scroll;
  animation-range: 0% 30%;
}
 
@keyframes header-fade {
  from {
    opacity: 1;
    transform: translateY(0);
  }
  to {
    opacity: 0;
    transform: translateY(-40px);
  }
}

Die Eigenschaft animation-range ist mächtig. Sie begrenzt, wann die Animation innerhalb der Scroll-Timeline läuft. animation-range: 0% 30% bedeutet, dass die Animation abgeschlossen ist, sobald der Nutzer 30 % des Containers gescrollt hat. So entstehen gestaffelte Effekte, bei denen verschiedene Elemente an unterschiedlichen Scroll-Positionen animiert werden.

View-Timelines: Elemente beim Eintreten animieren

View-Timelines lösen Animationen basierend auf der Sichtbarkeit eines Elements innerhalb seines scrollbaren Vorfahren aus. Das ersetzt das Intersection-Observer-Muster für Eingangsanimationen.

csscss
/* ❌ Old: Intersection Observer + class toggling */
/*
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        entry.target.classList.add('visible');
      }
    });
  });
  document.querySelectorAll('.animate-on-scroll').forEach(el => {
    observer.observe(el);
  });
*/
csscss
/* ✅ New: Pure CSS view timeline */
.card {
  animation: slide-up linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}
 
@keyframes slide-up {
  from {
    opacity: 0;
    transform: translateY(80px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

Die Funktion view() erzeugt eine Timeline basierend auf der Überschneidung des Elements mit seinem Viewport. animation-range: entry 0% entry 100% bedeutet, dass die Animation läuft, von dem Moment, in dem das Element erstmals in den Viewport eintritt, bis es vollständig sichtbar ist.

View-Timeline-Ranges haben benannte Phasen:

csscss
.staggered-reveal {
  animation: reveal linear both;
  animation-timeline: view();
}
 
/* Entry: element entering the viewport from below */
.entry-animation {
  animation-range: entry 0% entry 100%;
}
 
/* Cover: element covering the viewport (full visibility) */
.cover-animation {
  animation-range: cover 0% cover 100%;
}
 
/* Exit: element leaving the viewport from above */
.exit-animation {
  animation-range: exit 0% exit 100%;
}
 
/* Contain: element fully contained within viewport */
.contain-animation {
  animation-range: contain 0% contain 100%;
}
 
@keyframes reveal {
  from {
    opacity: 0;
    transform: translateY(60px) scale(0.95);
    filter: blur(4px);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
    filter: blur(0);
  }
}

Eine scroll-gesteuerte Hero-Section bauen

Die Kombination mehrerer scroll-gesteuerter Animationen ergibt reichhaltige, performante Hero-Sections ohne jegliches JavaScript.

htmlhtml
<section class="hero">
  <div class="hero-background"></div>
  <h1 class="hero-title">Build Better Software</h1>
  <p class="hero-subtitle">Engineering excellence at every layer</p>
  <div class="scroll-indicator">
    <span class="scroll-arrow"></span>
  </div>
</section>
 
<section class="content">
  <article class="feature-card">Feature One</article>
  <article class="feature-card">Feature Two</article>
  <article class="feature-card">Feature Three</article>
</section>
csscss
.hero {
  position: relative;
  height: 100vh;
  display: grid;
  place-items: center;
  overflow: hidden;
}
 
.hero-background {
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
  animation: bg-parallax linear both;
  animation-timeline: scroll();
  animation-range: 0vh 100vh;
}
 
@keyframes bg-parallax {
  to {
    transform: scale(1.2) translateY(-10%);
    filter: brightness(0.4);
  }
}
 
.hero-title {
  font-size: clamp(2rem, 6vw, 5rem);
  color: white;
  animation: title-exit linear both;
  animation-timeline: scroll();
  animation-range: 0vh 60vh;
}
 
@keyframes title-exit {
  to {
    opacity: 0;
    transform: translateY(-100px) scale(0.8);
    filter: blur(8px);
  }
}
 
.hero-subtitle {
  color: rgba(255, 255, 255, 0.7);
  animation: subtitle-exit linear both;
  animation-timeline: scroll();
  animation-range: 0vh 40vh;
}
 
@keyframes subtitle-exit {
  to {
    opacity: 0;
    transform: translateY(-60px);
  }
}
 
.scroll-indicator {
  position: absolute;
  bottom: 2rem;
  animation: fade-indicator linear both;
  animation-timeline: scroll();
  animation-range: 0vh 20vh;
}
 
@keyframes fade-indicator {
  to {
    opacity: 0;
  }
}
 
/* Feature cards enter with staggered view animations */
.feature-card {
  animation: card-enter linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 80%;
}
 
.feature-card:nth-child(2) {
  animation-delay: 0.1s; /* Stagger effect */
}
 
.feature-card:nth-child(3) {
  animation-delay: 0.2s;
}
 
@keyframes card-enter {
  from {
    opacity: 0;
    transform: translateY(60px) rotateX(10deg);
  }
  to {
    opacity: 1;
    transform: translateY(0) rotateX(0deg);
  }
}

Der Hero-Text blendet beim Scrollen mit unterschiedlichen Geschwindigkeiten aus und erzeugt so einen Tiefen-Parallax-Effekt. Der Hintergrund zoomt und dunkelt ab. Feature-Karten animieren sich, sobald sie in den Viewport eintreten. All das läuft auf dem Compositor-Thread.

Kombination mit der View Transitions API

Die View Transitions API ergänzt scroll-gesteuerte Animationen, indem sie weiche Übergänge zwischen Seitenzuständen oder Navigationen bereitstellt.

csscss
/* Opt-in to view transitions */
@view-transition {
  navigation: auto;
}
 
/* Style the transition pseudo-elements */
::view-transition-old(root) {
  animation: fade-out 0.3s ease-out;
}
 
::view-transition-new(root) {
  animation: fade-in 0.3s ease-in;
}
 
/* Named transitions for specific elements */
.product-image {
  view-transition-name: product-hero;
}
 
::view-transition-old(product-hero) {
  animation: scale-down 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
 
::view-transition-new(product-hero) {
  animation: scale-up 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
 
@keyframes scale-down {
  to {
    transform: scale(0.8);
    opacity: 0;
  }
}
 
@keyframes scale-up {
  from {
    transform: scale(1.2);
    opacity: 0;
  }
}
 
@keyframes fade-out {
  to {
    opacity: 0;
  }
}
 
@keyframes fade-in {
  from {
    opacity: 0;
  }
}

Progressive Enhancement und Fallbacks

Noch unterstützen nicht alle Browser scroll-gesteuerte Animationen. Mit @supports-Abfragen lassen sie sich als Progressive Enhancement über ein Grundgerüst legen, das überall funktioniert.

csscss
/* Base experience — works in all browsers */
.card {
  opacity: 1;
  transform: none;
  transition: opacity 0.3s, transform 0.3s;
}
 
/* Enhanced experience — scroll-driven animation */
@supports (animation-timeline: scroll()) {
  .card {
    animation: card-reveal linear both;
    animation-timeline: view();
    animation-range: entry 10% entry 90%;
  }
 
  @keyframes card-reveal {
    from {
      opacity: 0;
      transform: translateY(40px);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }
}
 
/* Respect motion preferences */
@media (prefers-reduced-motion: reduce) {
  .card {
    animation: none !important;
    opacity: 1;
    transform: none;
  }
}

Die Media Query prefers-reduced-motion ist nicht verhandelbar. Scroll-gesteuerte Animationen können bei Nutzern mit vestibulären Störungen Übelkeit auslösen. Biete immer eine Reduced-Motion-Alternative an, die Animationen entfernt oder minimiert.

Die wichtigsten Erkenntnisse

Scroll-gesteuerte CSS-Animationen beseitigen den größten Reibungspunkt der Web-Animation: die JavaScript-Abhängigkeit. Scroll-Fortschritts-Timelines, View-Timelines und Animation-Ranges geben dir präzise Kontrolle darüber, wann und wie Elemente relativ zur Scroll-Position animiert werden — alles auf dem Compositor-Thread mit 60 fps.

Der Wechsel des mentalen Modells ist erheblich. Statt „auf Scroll-Events lauschen und Positionen berechnen" definierst du Keyframes und deklarierst, welcher Scroll-Kontext sie antreibt. Der Browser übernimmt die Synchronisation.

Progressive Enhancement ist die Deployment-Strategie. Schreibe ein Basiserlebnis, das ohne Scroll-Animationen funktioniert, lege sie mit @supports darüber und respektiere immer prefers-reduced-motion. Die Nutzer mit dem erweiterten Erlebnis sehen butterweiche Interaktionen. Die anderen sehen weiterhin eine funktionierende Seite. So sollte das Web funktionieren.

Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX