Beobachten – asynchron im Hintergrund
Der Intersection Observer beobachtet Änderungen eines Elements im Verhältnis zu einem Eltern-Element – meist zum Viewport. Das geschieht asynchron – also ohne zu Blockieren im Hintergrund.
Wie weit überschneidet sich das Element mit einem umgebenden Element? So startet die Animation auf dem Punkt, die Seite ist schneller aufgebaut und ein Datentransfer, der u.U. nicht benötigt wird, findet gar nicht erst statt.Die Informationen über die Änderungen wurden früher in erster Linie für das Nachladen von Inhalten beim Scrollen (z.B. lazy loading images) benutzt, bis das loading-Attribut für das img-Tag auf breiter Basis von allen Browsern unterstützt wurde.
Intersection-Observer
Ein Intersection-Observer wird durch den Objekt-Konstruktor IntersectionObserver erstellt und hat zwei Argumente: Das erste ist eine Callback-Funktion, die ausgeführt wird, sobald ein Element in den Viewport kommt oder wenn sich der Abstand zwischen Elementen um einen gewissen Betrag geändert hat. Damit muss die Position eines Elements in Hinsicht auf ein anderes Element beim Scrollen nicht mehr permanent abgefragt werden.
Das zweite Argument listet die Optionen für den Intersection Observer.
callback function ──────┐ | ▼ const io = new IntersectionObserver (handleEntries, options); ▲ | Optionen ──────┘
Intersection Observer-Optionen
Mit den Default-Optionen des IntersectionObserver schaltet die Callback-Funktion, wenn ein Element teilweise in den Viewport kommt und den ViewPort verläßt. Das Observer API arbeitet nicht mit einem Pixelwert für das Überschneiden der Elemente, sondern reagiert, wenn die Überschneidung so irgendwie bei dem angegebenen Prozentsatz des threshold liegt.
<img id="simpleball" src="ball.png" alt="CSS Keyframe Animation »Rollender Ball«">
Wenn das Element in den Viewport kommt, weist der Intersection Observer dem Element die Klasse simpleanimate zu.
.simpleanimate { animation: simpleball 8s forwards; } @keyframes simpleball { from { transform: translateX(0) rotate(0deg) } to { transform: translateX(285px) rotate(360deg) } }
const target = document.getElementById("simpleball"); // Optionen konfigurieren const options = { root: null, // Der Viewport ist das Root-Element rootMargin: '0px', // Kein zusätzlicher Margin um das root threshold: 0.2 // 20% des Elements müssen sichtbar sein, um die Animation zu starten }; // Callback-Funktion definieren const callback = (entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add("simpleanimate"); } else { entry.target.classList.remove("simpleanimate"); } }); }; // Intersection Observer erzeugen const observer = new IntersectionObserver(callback, options); // Beobachten des Ziel-Elements starten observer.observe(target);
- root
- Das Elternelement oder der Viewport, in dem das Element liegt. null steht für den Viewport des Browsers.
- threshold
- ist ein Wert oder ein Array von Werten zwischen 0 und 1, bei denen der Intersection Observer die Callback-Funktion ausführt.
- rootMargin
- Beim Umfang des Überscheidungsbereichs (root) wird ein Abstand eingerechnet, ähnlich dem Margin bei CSS.
threshold und rootMargin
threshold = 0.5 würde beim Betreten und beim Verlassen des Root-Elements die Callback-Funktion aufrufen. Das könnte z.B. der Ablauf für ein Video sein, das bei Erscheinen im Viewport anläuft und pausiert, wenn es den Viewport verläßt.
Ist threshold ein Array – z.B. threshold: [0, 0.25, 0.5, 0.75, 1] –, wird die Callback-Funktion bei jedem der Werte aufgerufen (scrollen innerhalb der gelben Scrollbox).
Effekt: CSS transition-delay – Animation nach einer kurzen Verzögerung
CSS Animation starten
Mit der Feststellung, dass ~150px des Elements im Browserfenster sichtbar sind, startet eine Animation.
HTML
<figure class="image-container"> <div class="jade-scale"></div> <figcaption>CSS Transition und Transform Scale beim Scrollen</figcaption> </figure>
CSS transition und transform scale
.jade { background: url('marienkaefer-720.webp') no-repeat; background-size: cover; background-position: center center; transition: transform 5s ease; transform: scale(1); } .jade-scale.image-scaling { transform: scale(1.8); }
Und das Skript:
const jadeScale = document.querySelectorAll(".jade-scale"); const options = {rootMargin: "-150px"}; const jadeScaleObserver = new IntersectionObserver (function (entries, observer) { entries.forEach(function(entry) { if (entry.isIntersecting) { entry.target.classList.add("image-scaling"); } else { entry.target.classList.remove("image-scaling"); } }); }, options); jadeScale.forEach ( function (jadeScale) { jadeScaleObserver.observe (jadeScale); });
Die Option {threshold: 0.5} würde dafür sorgen, dass die Animation erst startet, wenn das Bild zu 50% im ViewPort ist.
Einfliegen von Links: Intersection Observer
Das Lazy Loading von Bildern, iframes und Video wird von Chrome, Edge, Firefox und Opera bereits durch ein einfaches HTML-Attribut eingesetzt: loading="lazy". Heute wird der Intersection Observer in erster Linie für das Starten von CSS-Animationen genutzt – z.B. das »fly in«, das Einfliegen eines Elements.
Wenn das Element mit der CSS-Klasse river in den Viewport kommt, setzt die Callback-Funktion eine zusätzliche Klasse swimming ein. Das Bild soll von links bis zur Mitte des blauen Bands einfliegen.
<div class="river"> <div class="crossing"> <img src="turtle-blue.svg" width="200" height="156" alt="turtle crossing"> </div> </div>
.swimming { animation: swimmer 10s ease-out forwards; } @keyframes swimmer { from {transform: translateX(calc(-200px ))} to {transform: translateX(calc(50% - 100px))} }
const river = document.querySelector (".river"); const crossing = document.querySelector (".crossing"); const turtleOptions = { root: null, rootMargin: "0px", threshold: 0.7, }; const callback = function (entries, observer) { const observed = entries[0]; if (observed.isIntersecting) { document.querySelector(".crossing").classList.add("swimming"); } } const observer = new IntersectionObserver (callback, turtleOptions); if (river) { observer.observe (river); }
Das Skript ist ziemlich einfach, das CSS hingegen ist trickreich. Ein transformiertes Element kennt nur sein eigenes Koordinatensystem und transform(translate: 100%) bezieht sich nur auf die Breite des Elements. Darum animiert das Skript nicht das Bild, sondern seinen Container mit der CSS-Klasse river.
Hintergrundbilder: Lazy Loading per Intersection Observer
Im einfachsten Fall – Bilder nachladen, wenn sie in den Viewport kommen –, braucht der Intersection Observer nur wenige Zeilen. Die Elemente der Slideshow haben eine zusätzliche CSS-Klasse lazy, das die CSS-Eigenschaft background-image: none setzt.
.lazy.slide.slide1, .lazy.slide.slide2, .lazy.slide.slide3, .lazy.slide.slide4 { background-image: none} .slide.slide1.inview { background: url(flowers--04.webp); } .slide.slide2.inview { background: url(flowers--03.webp); } .slide.slide3.inview { background: url(flowers--02.webp); } .slide.slide4.inview { background: url(flowers--01.webp); }
Wenn die Slideshow in den sichtbaren Ausschnitt des Browserfensters kommt, fügt das Script die Klasse inview hinzu, um die Hintergrundbilder zu laden.
Heute unterstützen alle modernen Browser das loading lazy-Attribut und der Intersection Observer wird für das Nachladen von Bildern nicht mehr gebraucht. Für Hintergrundbilder besteht diese einfache Möglichkeit nicht.
Der Platzhalter und die Angabe von width und height sind wichtig für ein stabiles Layout: Schwappt das Bild nachträglich in die Seite, käme es zu einem Cumulative Layout Shift (CLS) – das Layout verschiebt sich. Ärgerlich, wenn man gerade einen Text liest und noch ärgerlicher, wenn jetzt ein Klick zu einem falschen Link führt.
Wenn mehrere Elemente beobachtet werden sollen, sollten nach Möglichkeit alle Elemente vom selben IntersectionObserver durch mehrfache Aufrufe von observer () überwacht werden.
Script
const lazyBg = document.querySelectorAll(".slide"); const lazyBackgroundObserver = new IntersectionObserver (function (entries, observer) { entries.forEach(function(entry) { if (entry.isIntersecting) { entry.target.classList.add("inview"); } },{}); }); lazyBg.forEach ( function (lazyBackground) { lazyBackgroundObserver.observe(lazyBackground); });
Die Optionen in diesem Beispiel sind leer: Per Voreinstellung gilt der Viewport als Elternelement. Das Bild wird angezeigt, wenn es im Viewport sichtbar ist.
Bilder, die over the fold beim Laden der Seite angezeigt werden, sollten natürlich nicht per Observer geladen werden.
Wenn Bilder responsive mit HTML srcset eingebunden werden, kann die Callback-Funktion im IntersectionObserver die matchMedia-Abfrage einsetzen.
unobserve
Wenn das observierte Element nicht länger beobachtet werden soll – z.B. die Animation nach Ablauf nicht erneut starten soll oder Dateien nicht erneut geladen werden sollen, beendet die Methode unobserve den Intersection Observer.
if (entry.isIntersecting) { loadFile (entry) observer.unobserve(entry.target) }
Informationen des Intersection Observer
Zu den schönen Seite des Web Animation API zählt, dass die Browserkonsole und Debugging Einblick in den Ablauf der Animation geben.
Ein Blick in die Konsole zeigt gleich beim Laden der Seite, dass sich das Element noch nicht mit dem root-Element überschneidet (intersectionRatio: 0) und isIntersecting steht noch auf false.
boundingClientRect: DOMRectReadOnly {x: 369, y: 55.0625, width: 900, height: 622, top: 55.0625, …} intersectionRatio: 0 intersectionRect: DOMRectReadOnly {x: 369, y: 55.0625, width: 900, height: 621.9375, top: 55.0625, …} isIntersecting: false rootBounds: DOMRectReadOnly {x: 0, y: 0, width: 2019, height: 677, top: 0, …} target: <div class="simple"> time: 22273
Sobald das Element in den sichtbaren Ausschnitt des Browserfensters kommt, ist intersectionRatio auf 1 gesetzt und isIntersecting true.
boundingClientRect: DOMRectReadOnly {x: 369, y: 51.0625, width: 900, height: 622, top: 51.0625, …} intersectionRatio: 1 intersectionRect: DOMRectReadOnly {x: 369, y: 51.0625, width: 900, height: 622, top: 51.0625, …} isIntersecting: true rootBounds: DOMRectReadOnly {x: 0, y: 0, width: 2019, height: 677, top: 0, …} target: <div class="simple"> time: 31279
Relativ und fixed positionierte Intersection-Roots
Wenn Target-Elemente absolut innerhalb eines relativ positionierten Elements liegen, meldet der Intersection Observer keinen Treffer, wenn das Target-Element den relativ positionierten Root überschneidet.
Das gilt wohl auch für Target-Element mit position:fixed (?).
Browser-Support für IE
Zwar sind Browser ohne Javascript zu einer Seltenheit geworden, aber wenn sie dennoch beachtet werden sollen, kann z.B. ein noscript-Tag eingesetzt werden.
Die Abfrage, ob der IntersectionObserver im Window-Objekt vorhanden ist,
if (IntersectionObserver in window) { … … … }
hilft älteren Browsern, insbesondere IE, der das Intersection Observer-API nicht unterstützt. Falls nicht, lädt das Script die Merkmale direkt.
resizeObserver
Neben dem intersectionObserver gibt es ein weiteres Application Interface – resizeObserver, das genauso wie der Intersection Observer durch eine Callback-Funktion initialisiert wird.
Das ResizeObserver-Objekt beschränkt die Beobachtung auf einzelne Elemente, während das globale window.resize bei jeder Größenänderung des Viewports feuert und schnell zu Leistungseinbußen führt.