# Un bloc statistique animé au scroll, sans librairie externe

> Un compteur qui s'incrémente à l'affichage, écrit avec IntersectionObserver et requestAnimationFrame, sans ajouter un kilo-octet de dépendance JS.

- Auteur : Clément Hadrot
- Publié le : 2023-11-06
- Mis à jour le : 2023-11-06
- Catégorie : Blocs Gutenberg
- URL : https://wpmoderne.dev.wordpress-developpement.fr/blocs/bloc-statistique-anime-scroll-sans-librairie/

## L’essentiel

- IntersectionObserver plutôt qu'un scroll listener
- requestAnimationFrame pour l'incrément
- Respect de prefers-reduced-motion

Un client de Kaolin, une enseigne de matériel de sport, voulait un bloc « chiffres clés » affichant le nombre de magasins, de clients et d'années d'existence, avec un compteur qui s'anime dès que la section entre dans l'écran. Le brief tenait en une phrase : « comme sur tel site concurrent », accompagné d'un lien vers une page qui chargeait CountUp.js, GSAP et une bibliothèque de scroll-triggers pour trois malheureux chiffres.

Le résultat visuel est sympathique, mais le poids JS ne l'est pas. Pour un bloc aussi simple, deux API natives du navigateur suffisent largement : `IntersectionObserver` pour détecter l'entrée dans le viewport, et `requestAnimationFrame` pour l'incrément progressif. Voici comment nous l'avons construit.

## Le bloc : attributs et rendu dynamique

Le bloc est dynamique, rendu côté serveur, car les chiffres viennent souvent d'un champ de configuration global plutôt que d'être ressaisis à chaque page. Le `block.json` déclare un tableau de statistiques :

```
{
  "apiVersion": 3,
  "name": "kaolin/stat-block",
  "title": "Bloc statistique animé",
  "category": "widgets",
  "attributes": {
    "valeur": { "type": "number", "default": 0 },
    "libelle": { "type": "string", "default": "" },
    "suffixe": { "type": "string", "default": "" }
  },
  "supports": { "html": false },
  "render": "file:./render.php",
  "viewScript": "file:./view.js"
}
```

Le `render.php` sort un `<span>` avec la valeur finale en attribut `data-*`, et affiche `0` comme contenu initial : c'est ce que verra un visiteur si le JavaScript ne charge pas, ou un moteur de recherche.

## La détection d'entrée dans l'écran

Le `viewScript` ne s'exécute que sur le front, uniquement sur les pages qui contiennent le bloc, grâce au chargement conditionnel des scripts de blocs introduit avec `block.json`. Pas besoin de vérifier « est-ce que l'élément existe » sur toutes les pages du site.

> L'essentiel à retenir : IntersectionObserver plutôt qu'un scroll listener ; requestAnimationFrame pour l'incrément ; Respect de prefers-reduced-motion

```
document.querySelectorAll('.wp-block-kaolin-stat-block [data-valeur]').forEach((el) => {
  const observer = new IntersectionObserver((entrees) => {
    entrees.forEach((entree) => {
      if (entree.isIntersecting) {
        animerCompteur(el);
        observer.unobserve(el);
      }
    });
  }, { threshold: 0.4 });
  observer.observe(el);
});
```

Le `threshold: 0.4` déclenche l'animation quand 40 % de l'élément est visible, un compromis qui évite les déclenchements trop précoces sur mobile où la section peut être à moitié visible dès le chargement.

## L'incrément avec requestAnimationFrame

Plutôt qu'un `setInterval` qui égrène les valeurs à intervalle fixe (et donne un rendu saccadé sur les écrans à fréquence variable), `requestAnimationFrame` synchronise l'incrément avec le rafraîchissement de l'écran :

```
function animerCompteur(el) {
  const cible = parseFloat(el.dataset.valeur);
  const duree = 1200;
  const depart = performance.now();

  function etape(maintenant) {
    const progression = Math.min((maintenant - depart) / duree, 1);
    const valeurActuelle = Math.floor(progression * cible);
    el.textContent = valeurActuelle.toLocaleString('fr-FR');
    if (progression < 1) requestAnimationFrame(etape);
  }
  requestAnimationFrame(etape);
}
```

`toLocaleString('fr-FR')` insère les espaces insécables entre milliers, un détail que CountUp.js gère aussi, mais que trois lignes natives suffisent à reproduire.

## Respecter les préférences d'accessibilité

Une animation de plusieurs secondes peut gêner les personnes sensibles au mouvement. La media query `prefers-reduced-motion` permet d'afficher directement la valeur finale sans transition :

- Vérifier `window.matchMedia('(prefers-reduced-motion: reduce)').matches` avant de lancer l'animation.
- Si vrai, assigner directement `el.textContent` sans passer par `requestAnimationFrame`.
- Conserver le même rendu final dans les deux cas, seule la transition change.

## Ce que ce bloc ne fait pas

Il ne gère ni les animations d'apparition du reste de la mise en page (fondu, translation des blocs voisins), ni les scroll-triggers avec parallaxe : ce sont des besoins de mise en page globale, pas du ressort d'un bloc statistique isolé. Si un client demande ce type d'effet, mieux vaut évaluer une solution dédiée à l'échelle du thème plutôt que d'empiler des bibliothèques bloc par bloc, ce qui finit toujours par dupliquer le même moteur d'animation trois fois dans le même site.

> Chez Kaolin, la règle est simple : si l'effet visuel peut se coder en moins de cinquante lignes avec des API natives, on ne charge pas de librairie, même une petite.

## En résumé

Un compteur animé au scroll n'a pas besoin d'un framework d'animation dédié. `IntersectionObserver` détecte le bon moment, `requestAnimationFrame` assure une animation fluide et légère, et le chargement conditionnel des scripts de blocs garantit que ce code ne s'exécute que là où il sert. Sur ce projet, le bloc complet pèse un peu moins de deux kilo-octets minifiés, contre plus de soixante pour la solution avec bibliothèque initialement envisagée.
