aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Frontend & UI

Scroll-Animation: Performante Effekte mit Intersection Observer

Eine gute Scroll-Animation lässt Elemente genau im richtigen Moment erscheinen, während man die Seite nach unten scrollt — weder zu früh noch zu spät. Jahrelang haben wir das gebaut, indem wir auf das scroll-Event lauschten und jedes Frame von Hand berechneten. Der Browser kann das nun für uns übernehmen, ohne den Hauptthread zu belasten: IntersectionObserver. In diesem Leitfaden zeige ich Schritt für Schritt, wie man effizient erkennt, wann ein Element in den Viewport gelangt, wie man das mit CSS für flüssige Übergänge kombiniert und wie man die üblichen Fallstricke bei Performance und Barrierefreiheit vermeidet.

Warum Intersection Observer statt des Scroll-Events?

Der klassische Ansatz nutzte window.addEventListener('scroll', ...) und rief bei jedem Scrollschritt für jedes Element getBoundingClientRect() auf. Zwei Probleme: das Scroll-Event feuert Dutzende Male pro Sekunde, und getBoundingClientRect() zwingt den Browser, das Layout neu zu berechnen (Reflow). Zusammen verstopfen sie den Hauptthread und das Scrollen ruckelt.

Intersection Observer funktioniert umgekehrt: Du sagst dem Browser „benachrichtige mich, wenn dieses Element in den Sichtbereich kommt", und der Browser verfolgt das intern, asynchron und optimiert. Der Callback läuft nur, wenn sich der Schnittstatus ändert, nicht bei jedem Scroll-Frame. Das Ergebnis: weniger Code, weniger Reflow, eine flüssigere Seite.

Den ersten Observer einrichten

Die Grundverwendung besteht aus drei Teilen: den zu beobachtenden Zielelementen, einem Callback, der bei der Überschneidung läuft, und Optionen, die das Verhalten steuern.

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      entry.target.classList.add('is-visible');
      // nach dem Erscheinen die Beobachtung beenden
      observer.unobserve(entry.target);
    }
  });
}, {
  threshold: 0.2,        // feuert, wenn 20% des Elements sichtbar sind
  rootMargin: '0px 0px -10% 0px'
});

document.querySelectorAll('.reveal').forEach((el) => {
  observer.observe(el);
});

Der entscheidende Kniff hier: JavaScript fügt nur eine CSS-Klasse hinzu (is-visible). Die eigentliche Animation macht CSS. Diese Trennung ist sowohl für Performance als auch für Wartbarkeit entscheidend.

Die Animation dem CSS überlassen

Wir definieren den Start- und Endzustand des Elements in CSS und glätten den Übergang dazwischen mit transition. Die Verwendung von transform und opacity ist wichtig: Diese beiden Eigenschaften werden auf der GPU verarbeitet und lösen keine Layout-Neuberechnung aus.

.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

Mit derselben Struktur kannst du über verschiedene Klassen Varianten erzeugen, etwa Einschieben von links, Skalieren oder eine leichte Drehung. Ein weiterer Vorteil, die Animation in CSS zu halten: Selbst wenn JavaScript nicht läuft, sollte der Inhalt nicht bei opacity: 0 verborgen bleiben — das sichern wir im Abschnitt zur Barrierefreiheit ab.

threshold und rootMargin richtig einstellen

Diese beiden Optionen bestimmen vollständig, wie sich eine Scroll-Animation anfühlt:

  • threshold: ein Verhältnis zwischen 0 und 1. 0 feuert, sobald ein einziger Pixel sichtbar ist, 1, wenn das gesamte Element sichtbar ist. Du kannst auch ein Array übergeben ([0, 0.25, 0.5, 1]) und den Callback bei jeder Schwelle erhalten.
  • rootMargin: vergrößert oder verkleinert die Root-Box; verwendet die CSS-margin-Syntax. Ein negativer Unterwert wie -10% verzögert die Auslösung, bis das Element etwas weiter im Bildschirm ist, sodass sich die Animation „genau richtig" anfühlt.
  • root: standardmäßig der Viewport (null). Beobachtest du innerhalb eines scrollbaren Containers, übergib dieses Element als root.

Gestaffelter (Stagger) Einblendeffekt

Karten, die nacheinander mit leichter Verzögerung erscheinen, sind in modernen Oberflächen sehr beliebt. Du kannst das sauber umsetzen, indem du jedem Element über eine CSS-Variable eine Verzögerung zuweist:

const items = document.querySelectorAll('.reveal');
const io = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (!entry.isIntersecting) return;
    const i = Number(entry.target.dataset.index || 0);
    entry.target.style.setProperty('--delay', `${i * 80}ms`);
    entry.target.classList.add('is-visible');
    io.unobserve(entry.target);
  });
}, { threshold: 0.15 });

items.forEach((el, i) => {
  el.dataset.index = i;
  io.observe(el);
});

Auf der CSS-Seite bindest du diese Verzögerung an den Übergang: transition-delay: var(--delay, 0ms);. Vier Karten erscheinen dann nacheinander, in einer flüssigen Welle.

Fallstricke bei Performance und Barrierefreiheit

Einige Details machen das Projekt professionell:

  • Erneutes Auslösen steuern: Sobald ein Element erschienen ist, beende die Beobachtung mit unobserve(); sonst setzt sich die Animation zurück, wenn der Nutzer wieder nach oben scrollt. Wenn sie doch erneut abspielen soll, entferne die Klasse is-visible bei !entry.isIntersecting.
  • Bevorzugung reduzierter Bewegung: Manche Nutzer stört Bewegung. Verwende @media (prefers-reduced-motion: reduce), um Übergänge zu deaktivieren und Inhalte direkt anzuzeigen.
  • Inhalt nicht verborgen halten, wenn JavaScript aus ist: Wende den anfänglichen verborgenen Zustand nur an, wenn JS aktiv ist. Ein praktischer Ansatz: Füge document.documentElement.classList.add('js') zum <html>-Element hinzu und bedinge das CSS als .js .reveal { opacity: 0; }.
@media (prefers-reduced-motion: reduce) {
  .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

Häufige Fragen

Wie steht es um die Browserunterstützung von Intersection Observer?

Er wird seit Jahren in allen aktuellen Browsern (Chrome, Firefox, Safari, Edge) unterstützt; keine separate Bibliothek nötig. Solange du keine sehr alten Browser anvisierst, brauchst du auch kein Polyfill.

Ist das Lauschen auf Scroll mit requestAnimationFrame besser?

Für kontinuierliche Effekte, die sich während des Scrollens verändern, etwa Parallax, ist requestAnimationFrame passend. Aber um zu wissen, ob ein Element sichtbar ist, ist Intersection Observer immer die effizientere und einfachere Lösung.

Sollte die Animation bei jedem Scrollen erneut abspielen?

Meist nicht. Lass Inhalte einmal erscheinen und so bleiben; ständig wiederkehrende Einblendanimationen lenken ab und belasten die Performance. In seltenen Fällen kannst du sie wiederholen, aber unobserve() ist die empfohlene Voreinstellung.

Möchtest du flüssige, performante Scroll-Animationen auf deiner Seite? Für barrierefreie, ruckelfreie Interface-Effekte, abgestimmt mit den richtigen Schwellenwerten, nimm Kontakt mit mir auf.

Bu kategorideki tüm yazılar →

Devamı için