À propos de ce cours
Apprenez à détecter efficacement quand un élément devient visible avec l'API Intersection Observer, au lieu de l'événement scroll : création d'un observateur, options root, rootMargin et threshold, propriétés des entrées, et un exemple complet de fil d'actualités avec animations d'apparition, images chargées à la demande et défilement infini. Avec d'autres cas d'usage et des activités corrigées.
-
Objectifs
- Comprendre pourquoi l'événement
scrolln'est pas adapté pour savoir si un élément est visible. - Créer un
IntersectionObserveret régler ses optionsroot,rootMarginetthreshold. - Réaliser trois effets courants : apparition au défilement, images chargées à la demande et défilement infini.
- Comprendre pourquoi l'événement
-
Le problème : savoir si un élément est visible
- Beaucoup de pages modernes réagissent au défilement : animations qui apparaissent, images chargées au dernier moment, liste qui se complète en bas de page (réseaux sociaux, boutiques en ligne).
- L'ancienne méthode consistait à écouter l'événement
scrollet à calculer la position de chaque élément avecgetBoundingClientRect():
// À éviter : exécuté des dizaines de fois par seconde window.addEventListener("scroll", () => { for (const element of elements) { const rect = element.getBoundingClientRect(); if (rect.top < window.innerHeight && rect.bottom > 0) { element.classList.add("visible"); } } });- Ce code s'exécute à chaque pixel défilé, oblige le navigateur à recalculer la mise en page et rend la page saccadée, surtout sur téléphone (voir la leçon sur debounce et throttle, chapitre 23).
- L'API Intersection Observer résout ce problème : on lui confie des éléments, et c'est le navigateur qui nous prévient, de façon optimisée, quand ils entrent dans l'écran ou en sortent.
-
Créer un observateur
const observateur = new IntersectionObserver(callback, options);
observateur.observe(element); // commencer à surveiller un élément
observateur.unobserve(element); // arrêter de le surveiller
observateur.disconnect(); // tout arrêter
- La fonction
callback(entrees, observateur)reçoit un tableau d'objetsIntersectionObserverEntry, un par élément dont la visibilité a changé. Elle est aussi appelée une première fois juste aprèsobserve(). - Propriétés utiles de chaque entrée :
| Propriété | Signification |
|---|---|
target | L'élément observé |
isIntersecting | true si l'élément est (au moins en partie) dans la zone |
intersectionRatio | Part visible de l'élément, de 0 à 1 |
boundingClientRect | Position et taille de l'élément |
Les options
| Option | Par défaut | Rôle |
|---|---|---|
root | null (l'écran) | Zone de référence : l'écran ou un élément qui défile (une liste avec overflow: auto). |
rootMargin | "0px" | Agrandit (valeur positive) ou réduit (négative) la zone, comme une marge CSS : "200px 0px" déclenche 200 px avant que l'élément n'arrive à l'écran. |
threshold | 0 | Part visible qui déclenche l'appel : 0 (le premier pixel), 0.5 (la moitié), 1 (entièrement visible), ou une liste [0, 0.5, 1]. |

-
Exemple complet : un fil d'actualités
-
Usage 1 : animation d'apparition
- Les articles sont invisibles et décalés vers le bas en CSS ; l'observateur leur ajoute la classe
visible, et latransitionCSS fait le reste. - Après l'apparition,
unobserve()arrête la surveillance de l'élément : l'animation ne se rejoue pas et le navigateur a moins de travail. - Accessibilité : respectez le réglage
prefers-reduced-motiondes personnes sensibles aux animations (exemple dans le TP 21).
- Les articles sont invisibles et décalés vers le bas en CSS ; l'observateur leur ajoute la classe
-
Usage 2 : images chargées à la demande
- L'adresse de l'image est rangée dans
data-src; elle est copiée danssrc(ce qui lance le téléchargement) quand l'image arrive à 200 px de l'écran grâce àrootMargin. - Bon à savoir : pour une simple image, HTML fait aujourd'hui la même chose sans JavaScript avec l'attribut
loading="lazy":<img src="photo.jpg" loading="lazy" alt="…">. Utilisez-le en priorité, et l'observateur pour les cas plus complexes (arrière-plans CSS, vidéos, composants, publicités…). - Réservez toujours la place de l'image (
width/heightouaspect-ratio) pour éviter que la page ne « saute » au chargement.
- L'adresse de l'image est rangée dans
-
Usage 3 : défilement infini
- Un élément vide placé après la liste, la sentinelle, est observé. Quand il devient visible, on ajoute le paquet suivant : la sentinelle est repoussée plus bas, et ainsi de suite.
- Quand il n'y a plus rien à charger,
disconnect()arrête l'observateur. - Dans une vraie application,
chargerPage()appelle une API avecfetchet un numéro de page (?page=3). Pensez à empêcher deux chargements simultanés avec une variableenCours. - Prévoyez aussi un bouton « Charger plus » : le défilement infini empêche d'atteindre le pied de page et gêne la navigation au clavier.
-
Autres usages
- Sommaire actif: mettre en surbrillance, dans un menu, la section en cours de lecture.
- Vidéos: lancer la lecture quand la vidéo est visible, la mettre en pause sinon.
- Statistiques: savoir si une publicité ou un bloc a vraiment été vu (
threshold: 0.5pendant 1 seconde). - En-tête collant: changer le style du menu quand la bannière du haut sort de l'écran.
-
Activités
La page affiche 30 actualités, chargées par paquets de 5 quand on arrive en bas. Chaque article apparaît en fondu et son image n'est téléchargée qu'au moment où il s'approche de l'écran. Trois observateurs, trois usages.
index.html:
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<title>Actualités – défilement infini</title>
<link rel="stylesheet" href="style.css" />
<script src="observer.js" defer></script>
</head>
<body>
<h1>Actualités du centre</h1>
<div id="liste"></div>
<p id="sentinelle">Chargement…</p>
</body>
</html>
style.css:
.article {
border: 1px solid #ccd;
border-radius: 8px;
padding: 16px;
margin: 12px 0;
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s, transform 0.6s;
}
.article.visible {
opacity: 1;
transform: none;
}
.article img {
width: 100%;
aspect-ratio: 16 / 9;
background: #eef2f7; /* réserve la place avant le chargement */
}
observer.js:
const liste = document.getElementById("liste");
const sentinelle = document.getElementById("sentinelle");
const PAR_PAGE = 5;
const TOTAL = 30;
let page = 0;
// 1. Faire apparaître chaque article quand il entre dans l'écran
const apparition = new IntersectionObserver(
(entrees) => {
for (const entree of entrees) {
if (entree.isIntersecting) {
entree.target.classList.add("visible");
apparition.unobserve(entree.target); // une seule fois
}
}
},
{ threshold: 0.15 }
);
// 2. Charger l'image seulement quand l'article approche (200 px avant)
const chargementImages = new IntersectionObserver(
(entrees, observateur) => {
for (const entree of entrees) {
if (entree.isIntersecting) {
const img = entree.target;
img.src = img.dataset.src;
observateur.unobserve(img);
}
}
},
{ rootMargin: "200px 0px" }
);
function creerArticle(numero) {
const article = document.createElement("article");
article.className = "article";
const titre = document.createElement("h2");
titre.textContent = `Actualité n° ${numero}`;
const img = document.createElement("img");
img.alt = `Illustration de l'actualité ${numero}`;
img.dataset.src = `https://picsum.photos/seed/${numero}/640/360`;
article.append(titre, img);
apparition.observe(article);
chargementImages.observe(img);
return article;
}
function chargerPage() {
const debut = page * PAR_PAGE + 1;
for (let numero = debut; numero < debut + PAR_PAGE && numero <= TOTAL; numero++) {
liste.append(creerArticle(numero));
}
page++;
if (page * PAR_PAGE >= TOTAL) {
sentinelle.textContent = "Vous avez tout lu 🎉";
defilementInfini.disconnect();
}
}
// 3. Défilement infini : quand la sentinelle devient visible, on charge la suite
const defilementInfini = new IntersectionObserver((entrees) => {
if (entrees[0].isIntersecting) {
chargerPage();
}
});
defilementInfini.observe(sentinelle);
Activité 1 : reproduire et observer
- Dans le dossier
observerde votre dépôtjs-api-navigateur, reproduisez l'exemple. Dans l'onglet Réseau des DevTools (filtre Img), vérifiez que les images ne sont téléchargées qu'au défilement. - Remplacez
rootMargin: "200px 0px"par"0px", puis par"-100px 0px". Que constatez-vous ? - Remplacez
threshold: 0.15par1. Que se passe-t-il pour un article plus haut que l'écran ?
Activité 2 : sommaire actif
- Créez une longue page de cours avec 5 sections
<section id="…">et un menu fixe à gauche. Le lien de la section visible doit recevoir la classeactive. - Astuce :
rootMargin: "-40% 0px -55% 0px"ne considère qu'une bande au milieu de l'écran.
Activité 3 : compteurs animés
- Affichez trois chiffres clés du centre (stagiaires, formateurs, taux de réussite). Quand le bloc devient visible à 50 %, les nombres montent de 0 à leur valeur en 1,5 seconde (
requestAnimationFrame). L'animation ne se joue qu'une fois.
Activité 4 : questions
- Pourquoi l'observateur est-il plus performant qu'un écouteur
scroll? - À quoi sert
unobserve()dans l'animation d'apparition ? - Quel attribut HTML permet de différer le chargement d'une image sans JavaScript ?
Corrigés. Activité 1 : avec
"0px", les images se chargent au moment où elles entrent dans l'écran (on les voit apparaître) ; avec "-100px 0px", il faut qu'elles soient entrées de 100 px. Avec threshold: 1, un article plus haut que l'écran n'est jamais visible à 100 % et n'apparaît donc jamais. Activité 4 : le navigateur calcule lui-même les intersections de façon optimisée et n'appelle la fonction que lors d'un changement, au lieu de l'appeler à chaque pixel défilé ; unobserve() évite de rejouer l'animation et allège le travail du navigateur ; loading="lazy". À retenir
new IntersectionObserver(callback, { root, rootMargin, threshold })puisobserve(element).- Dans la fonction, on teste
entree.isIntersectinget on agit surentree.target. - Trois usages classiques : apparition au défilement, chargement différé, défilement infini avec une sentinelle.
- Pour une image simple, préférez
loading="lazy".
Sources : MDN Web Docs, web.dev, W3C, WHATWG, open-meteo.com.