Skip to content

Intersection Observer

Intersection Observer API auf mdn

Die Intersection Observer API nutzen wir, um herauszufinden, ob (bzw. wann) sich zwei Elemente überschneiden. Wir verwenden das z.B.

  • für einen Header, der am oberen Bildschirmrand fixiert wird, und der die Hintergrundfarbe ändern soll, sobald ein nachfolgendes Element unter den Header scrollt
  • für Elemente, die in den Viewport hineinsliden, sobald das Element den Viewport betritt
  • für Bilder, die erst dann geladen werden, wenn sie im Viewport sichtbar werden (bzw. kurz bevor sie sichtbar werden)

Um den Intersection Observer zu verwenden, brauchen wir also Minimum zwei Elemente:

  • ein Element, das beobachtet werden soll (das Zielelement bzw. das target)
  • ein Element, das überschnitten wird (das kann ein Element sein oder, wenn keines definiert ist, der Viewport, das ist das Wurzelelement bzw. das root)

Als erstes deklarieren wir für beide Elemente eine Variable:

// unser root-Element
const root = document.querySelector('.root');
// unser target-Element
const target = document.querySelector('.target');

Die Intersection Observer API erlaubt es uns, eine Callback-Funktion zu schreiben. Diese wird aufgerufen, wenn

  1. eine Überschneidung (die in der Callback-Funktion genauer definiert wird) beobachtet wird (das kann öfter passieren, je nachdem, wieviele Elemente beobachtet werden), und
  2. die observe-Methode erstmals aufgerufen wird (das passiert nur ein Mal)

Dazu instanziieren wir als erstes einen neuen Intersection Observer.

Wir rufen den Konstruktur mit new auf und übergeben ihm die oben angesprochene Callback-Funktion, die er ausführen soll, sobald er eine Überschneidung feststellt:

const observer = new IntersectionObserver(callback, options);

Mit dem options-Objekt, das dem IntersectionObserver()-Konstruktor übergeben wird, können wir die genaueren Umstände definieren, wann die Callback-Funktion aufgerufen wird.

const options = {
root: null, // default
rootMargin: '0px 0px 0px 0px', // default
scrollMargin: '0px', // default
threshold: 0, // default
}
  • root: Dieses Element fungiert als Viewport, in dem die Sichtbarkeit des Zielelementes geprüft wird. Es muss ein Vorfahr des Zielelements sein. Wenn null oder nicht angegeben, ist das root-Element gleich dem Browser Viewport.
  • rootMargin: wie mit CSS-Margin (Werte-Reihenfolge top, right, bottom, left) können die Grenzen des Root-Elements nach innen (negative Werte) oder nach außen (positive Werte) verschoben werden. Die Zone, in der die Überschneidung kontrolliert wird, kann damit ausgedehnt oder verringert werden. Erlaubt sind nur Pixel oder Prozentwerte.
  • scrollMargin: ein Margin um verschachtelte Scroll-Container, gleich wie rootMargin.
  • threshold: Grenzwert, kann einen Wert zwischen 0 (Elemente überschneiden sich nicht) und 1 (Element ist vollständig im Viewport des root-Elements). 1 kann kritisch werden, wenn das target-Element sehr groß ist und somit nie als ganzes sichtbar sein kann (Mobilversionen beachten!). threshold kann eine Zahl sein oder ein Array von Werten. Z.B. könnte man benachrichtigt werden wollen, wenn ein Element zu 25%, zu 50%, zu 75% und zu 100% im Überschneidungsbereich liegt.

Die Callback-Funktion, die dem IntersectionObserver()-Konstruktur übergeben wird, erhält eine Liste von IntersectionObserverEntry-Objekten und den observer selbst:

const callback = (entries, observer) => {
entries.forEach((entry) => {
// jede Erfassung beschreibt eine Änderung der Überschneidung für ein beobachtetes target-Element:
entry.boundingClientRect
entry.intersectionRatio
entry.intersectionRect
entry.isIntersecting
entry.rootBounds
entry.target
entry.time
});
}

Für jedes Grenzübertritt-Event empfängt die Callback-Funktion ein IntersectionObserverEntry-Objekt. Mehrere gleichzeitige Erfassungen (entry) sind möglich, entweder von verschiedenen Zielelementen oder von einem einzelnen Element, das mehrere Grenzen innerhalb eines kurzen Zeitraums überschreitet. Die Events werden in eine Schlange eingereiht, werden also nach der Zeit, in der sie generiert wurden, geordnet.

Jedes entry beschreibt, wie weit das aktuell erfasste Element mit dem root-Element überlappt, ob das Element überhaupt überlappt etc.

Für uns ist erst einmal das entry.isIntersecting-Objekt interessant. Abhängig davon, ob das target-Element das root-Element überlappt oder nicht (und wie weit), können wir Dinge erledigen lassen:

const callback = (entries, observer) {
entries.forEach((entry) => {
// immer erst einmal prüfen, ob alles sitzt:
console.log(entry);
if (entry.isIntersecting) {
// eventuell nochmal das Zielelement selbst loggen:
console.log(entry.target);
// do sth.
}
});
}

Zuletzt müssen wir noch den Observer aufrufen und ihm sagen, welches Element er beobachten soll:

observer.observe(target);
// Jetzt wird, wie oben schon beschrieben, die Callback-Funktion erstmals aufgerufen um die Situation abzuchecken, in weiterer Folge immer dann, wenn ein Übertritt beobachtet wird.

IntersectionObserver.unobserve() stoppt die Beobachtung eines bestimmten Zielelements.

const root = document.querySelector('.root');
const target = document.querySelector('.target');
const options = {
rootMarginTop: '90px',
}
const callback = (entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
root.classList.add('.intersected');
}
});
}
const observer = new IntersectionObserver(callback, options);
observer.observe(target);

A graphical introduction to the Intersection Observer API

Was wir minimal tun müssen:

  • einen Observer instantiieren
  • ein target observieren
const callback = (entries, observer) => {};
// ohne Optionen werden die default-Werte verwendet:
// {threshold: 0, rootMargin: '0px 0px 0px 0px'}
const observer = new IntersectionObserver(callback);
const target = document.querySelector('.blubb');
observer.observe(target);

Diese Methode sagt dem Observer, dass er

  • das target tracken und
  • sobald eine Intersection passiert, die Callback-Funktion ausführen soll.

Die Observer-Instanz wird basierend auf den Optionen, die beim instanziieren injected wurden, ausgeführt.

Wenn mehrere Ziele getrackt werden sollen, geht das ganz einfach mit einem Array und einer forEach-Schleife:

const multipleTargets = [...document.querySelectorAll('.blubb')];
multipleTargets.forEach((target) => observer.observe(target));

Wenn wir einen IntersectionObserver mit new instantiieren, können wir den zweiten Parameter, die Optionen, weglassen, und nur die Callback-Funktion übergeben. Aber mit Optionen können wir mehr Möglichkeiten ausschöpfen:

const observer = new IntersectionObserver(callback, {
threshold: 1,
rootMargin: '-100px 0px -250px 0px',
});

Die Callback-Funktion ist asynchron und kann eine gute, alte JavaScript-Funktion mit zwei Parametern sein:

const callback = (entries, observer) => {
entries.forEach((entry) => {
// Validate the entry's intersection here and make decision
});
}
  • entries: Eine Liste von IntersectionObserverEntry-Objekten, oder, anders gesagt, eine Liste von Elementen, die bezüglich einer Intersection observiert werden, begleitet von relevanten Daten, um Entscheidungen über die Intersection zu machen (unter anderem isIntersecting, intersectionRatio, target, …)
  • observer: die Instanz des IntersectionObserver, der das Element observiert, das die Intersection gerade festgestellt hat. Wird meist mit unobserve benutzt, wenn die Aktion nur einmal ausgeführt werden soll.

In der Callback-Funktion spielt sich der ganze Zauber ab. Dort werden wir über die überlappenden Ziele benachrichtigt, basierend auf unseren Einstellungen. Aber es ist wichtig zu wissen, dass die Callback-Funktion in zwei verschiedenen Szenarien ausgeführt wird:

  1. Unmittelbar nach dem Aufruf der observe-Methode versucht der Browser eine untätige Periode zu finden und die Callback-Funktion ausführen, unabhängig davon, ob sich das target mit dem root überschneidet oder nicht. Das bietet die Gelegenheit, frühzeitig festzustellen, ob sich Elemente überlappen (oder nicht) und entsprechend den IntersectionObserverEntry-Parametern zu entscheiden. Dieser Vorgang läuft nur einmal ab.
  2. Jedes Mal, wenn ein target sich mit dem root, basierend auf den konfigurierten Optionen, überschneidet. Das kann mehrere Male passieren.

Immer wenn die Callback-Funktion ausgeführt wird, wird ein Array von Entries empfangen. Dafür können verschiedenste Attribute verwendet werden, aber das sind die häufigsten/wichtigsten:

  • isIntersecting: ein boolscher Wert, der wahr wird, wenn target sich mit dem root überschneidet.
  • intersectionRatio: ein Zahlenwert number zwischen 0 und 1, die die Position des target, das sich mit den Grenzen von root überlappt, zur Verfügung stellt. Das ist nützlich, wenn das threshold eine Liste von Zahlen ist.
  • target: element wo eine Überlappung mit dem root festgestellt wurde.

Wie können wir also ein Element erkennen, das gerade dabei ist, sichtbar zu werden, und ihm eine Klasse geben, damit es einfadet, sobald es in den Viewport kommt? Ungefähr so:

const callback = (entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.toggle('blubb-visible');
}
});
}
const observer = newIntersectionObserver(callback, {
threshold: 0,
rootMargin: '-100px 0px -100px 0px'
});
const elementsAboutToBeVisible = [...document.querySelectorAll('.blubbblubb')];
elementsAboutToBeVisible.forEach((element) => observer.observe(element));

Zum Abschluss können wir noch die Observation des Elements stoppen, da es jetzt sichbar ist und wir nicht mehr damit interagieren müssen:

const callback = (entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.toggle('.blubbblubb');
observer.unobserve(entry.target);
}
});
}

Diese Methode ist das Gegenteil von observe. Das bedeutet, wir können aufhören, ein Element zu tracken.