vendredi 25 septembre 2026

À propos

Contact

Blocs Gutenberg

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.

Par Clément Hadrot • 6 novembre 2023 • 4 min de lecture • Aucun commentaire
Un bloc statistique animé au scroll, sans librairie externe

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.

Partager :

À propos de l'auteur

Clément Hadrot

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi