ScrollGratuit

Introduction

Un conteneur de 280 px qui défile, une piste de 4 px collée en haut par position: sticky et quatre lignes de JavaScript qui écrivent scrollTop ÷ (scrollHeight − clientHeight) dans sa largeur : 0 % en haut, 100 % au dernier pixel, mesuré à la proportion exacte. Le pourcentage est calculé mais jamais affiché — l'écrire en texte tient en une ligne.

CSSJavaScriptUX

Introduction

Le scrollytelling est une technique narrative qui utilise le defilement de la page pour guider l'utilisateur a travers une histoire visuelle immersive.

Chapitre 1

Les animations declenchees par le scroll permettent de reveler progressivement le contenu, creant un rythme naturel de lecture qui captive l'attention.

Chapitre 2

En combinant parallaxe, transitions et effets visuels, chaque section devient une scene dans un recit plus large, transformant un simple site web en experience narrative.

Chapitre 3

Les meilleurs exemples de scrollytelling equilibrent esthetique et fonctionnalite, guidant l'utilisateur sans le submerger d'effets excessifs.

Conclusion

Maitrisez ces techniques pour creer des experiences web memorables qui racontent votre histoire avec elegance et fluidite.

Scrollez pour explorer
Survolez ou cliquez la scène pour interagir

Cet effet est gratuit — la catégorie Scroll en contient 70 au total, dont 12 gratuits. Effect.Labs regroupe 811 effets vanilla. Découvrir la catégorie →

Exemples d'usage

Rapport d'impact 2025
ONG Rives & Sources — Eau, assainissement, santé
47 % lus
Chapitre 6 / 12

Édito

En 2025, 214 000 personnes ont eu accès à une eau potable durable grâce à 38 projets menés avec les communautés du bassin du Niger et du delta du Mékong.

Résultats terrain

126 forages réhabilités, 41 comités d'usagers formés, un taux de fonctionnement des points d'eau passé de 68 % à 91 % en dix-huit mois.

Accès à l'eau

Chaque forage est confié à un comité local qui fixe le tarif, tient la caisse et commande les pièces : la maintenance ne dépend plus de nous.

Assainissement

9 200 latrines familiales construites selon l'approche communautaire ; 63 villages certifiés « fin de défécation à l'air libre » par les autorités sanitaires.

Éducation

Dans 87 écoles, les clubs d'hygiène animés par les élèves ont réduit l'absentéisme de 12 % pendant la saison des pluies.

Santé

Les cas de diarrhée chez les enfants de moins de cinq ans ont baissé de 31 % dans les zones couvertes, selon les registres des centres de santé.

Bilan financier

Budget exécuté : 8,4 M€, dont 79 % affectés aux programmes, 13 % au suivi-évaluation et 8 % aux frais de structure. Comptes certifiés sans réserve.

Gouvernance

Le conseil d'administration compte désormais 40 % de membres issus des pays d'intervention, conformément à l'engagement pris en 2023.

Témoignages

« Avant, je marchais deux heures pour l'eau. Aujourd'hui la borne est au bout de la rue et mes filles vont à l'école. » — Aminata, présidente d'un comité d'usagers.

Partenaires

27 partenaires locaux, 4 bailleurs institutionnels et 11 400 donateurs réguliers ont rendu ces résultats possibles.

Perspectives 2026

Ouverture d'un programme au Sahel oriental, extension du suivi par capteurs à 300 forages et publication trimestrielle des données d'exploitation.

Remerciements

À toutes celles et ceux qui, sur le terrain ou à distance, ont porté ces projets : ce rapport est le vôtre.

① Rapport d'impact annuel d'une ONG — douze chapitres, « 47 % lus »

QuandRapport de durabilité ou d'activité lu dans un conteneur : résultats terrain, bilan financier, témoignages, en douze chapitres séparés par les .section-divider du code.
PourquoiUn document formel se lit d'une traite ; la barre suit le geste au pixel et le pourcentage — calculé par le code, affiché par une ligne ajoutée — dit où en est le lecteur : « 47 % lus », chapitre 6 sur 12. Scroll Progress Beam (fx-0793) donne un indicateur lumineux latéral sans valeur chiffrée ; ici le chiffre est la donnée.
RéglagesLe cadre de la scène (en-tête + document) tient lieu de cadre .st-progress-scroll ; conteneur passé de 280px à flex: 1 ; scène ouverte à 47 % de la course (position mémorisée) ; libellé « 47 % lus » et compteur de chapitre calculés dans le même écouteur scroll ; dégradé d'origine.

② Notice légale à lecture attestée — le bouton s'active à 100 %

QuandPortail administratif : conditions d'utilisation d'un logiciel métier que chaque agent doit parcourir avant de valider ; huit articles, bouton « J'atteste avoir lu ».
PourquoiLa preuve de lecture doit être mesurable : scrollTop ÷ course donne un nombre, pas une impression. Le bouton reste disabled tant que le pourcentage n'atteint pas 100 ; aucun autre effet du lot ne combine progression chiffrée et texte long libre.
RéglagesCadre de la scène (en-tête + document + pied) à la place du cadre vendu ; remplissage en bleu uni #3b82f6 à la place du dégradé ; pied de scène avec le pourcentage « % lu » et le bouton, déverrouillé dans l'écouteur quand progress ≥ 99.5 ; libellé pulsant retiré.
Formation CPF · Module 1 sur 6
Introduction à la gestion de projet
0 % lu
Lecture libre · environ 6 min

Objectifs du module

À la fin de ce chapitre, vous saurez définir un projet, distinguer ses trois contraintes et reconnaître les rôles d'une équipe projet.

Qu'est-ce qu'un projet ?

Un projet est un effort temporaire, avec un début et une fin, entrepris pour produire un résultat unique : un produit, un service ou un changement d'organisation.

Le triangle des contraintes

Périmètre, délai et budget se tiennent : modifier l'un déplace les deux autres. Un chef de projet passe son temps à rendre ces arbitrages visibles.

Les rôles

Le commanditaire décide et finance, le chef de projet organise et arbitre, l'équipe réalise, les parties prenantes sont consultées aux jalons.

Exemple : refonte d'un site intranet

Périmètre : 40 pages et un annuaire. Délai : 12 semaines. Budget : 3 jours-personne par semaine. Quel arbitrage si l'annuaire prend deux fois plus de temps ?

À retenir

Un projet est temporaire et unique ; ses trois contraintes se compensent ; chaque rôle a une responsabilité claire. Le module 2 traite de la planification.

Faites défiler pour lire

③ Module de cours en ligne à lecture libre

QuandChapitre introductif d'une formation (CPF, onboarding, tutoriel) dans une application web : objectifs, notions, exemple, à retenir.
PourquoiLe contenu reste un texte continu que l'apprenant parcourt et reparcourt librement — la barre encourage à aller au bout sans imposer d'arrêt. Snap + Progress (fx-0597) force des paliers ; ils conviennent à des étapes distinctes, pas à un cours qu'on relit.
RéglagesLe span.scroll-hint-label du code conservé (« Faites défiler pour lire »), calé en bas du cadre de la scène, qui joue le rôle du cadre .st-progress-scroll vendu ; padding-bottom: 40px sur .progress-inner pour qu'il ne couvre pas la dernière ligne ; lecture « % lu » dans l'en-tête.

Comment ça marche

Le HTML vendu tient en un bloc. Un cadre .st-progress-scroll enveloppe tout : c'est lui qui porte le fond #0a0a0f, la couleur du texte et la police (Inter, puis la police système), qui est position: relative pour servir de repère au libellé placé en absolu, et qui est une colonne flex à max-height: 100% — vos couleurs et votre police se posent sur cette seule règle. Dedans, .scroll-container#progress-scroll (width: 100%, height: 280px, overflow: hidden auto — pas de défilement horizontal, vertical automatique —, position: relative, scroll-behavior: smooth) contient d'abord la piste .st-progress-bar-track (4 px de haut, blanc à 8 %, position: sticky; top: 0, z-index: 20) et son remplissage .st-progress-bar-fill#progress-fill (width: 0%, dégradé #6366f1 → #a855f7 → #ec4899, coins droits arrondis de 2 px, transition: width 0.05s linear), puis le corps .progress-inner (padding: 24px 20px) : cinq h3 de 1 rem en blanc, cinq p de 0,8 rem en blanc à 50 % avec un interligne de 1,7, quatre .section-divider de 60 × 2 px. Leurs marges sont écrites en entier (margin: 0 0 12px pour les titres, 0 0 16px pour les paragraphes) : le rendu ne dépend pas d'un reset de la feuille de styles qui accueille le bloc. Comme la piste est dans l'élément qui défile, sticky la garde collée au bord haut pendant que le texte passe dessous — mesuré : la piste reste à y = 0 du conteneur en fin de course. Après le conteneur, toujours dans le cadre, un span.scroll-hint-label « Scrollez pour explorer » en 0,65 rem capitales, blanc à 30 %, calé à 8 px du bas du cadre, pulse de 0,3 à 0,7 d'opacité toutes les 2 s (@keyframes pulseHint). Les barres de défilement WebKit sont réduites à 4 px (::-webkit-scrollbar).

Le script fait une chose. Enfermé dans une fonction immédiatement invoquée — progressScroll et progressFill n'existent pas dans window et ne peuvent pas entrer en collision avec vos propres variables —, il récupère les deux éléments par leur id et, s'ils existent, pose un seul écouteur scroll sur le conteneur — pas de requestAnimationFrame, pas de boucle : rien ne tourne au repos. À chaque événement, scrollHeight − clientHeight donne la course totale en pixels (215 px pour le contenu vendu à 1 264 px de large, 454 px à 374 px, les paragraphes se repliant), scrollTop / course × 100 donne le pourcentage, écrit tel quel dans style.width. La transition de 50 ms lisse le pas entre deux événements. Mesuré dans Chromium sur une page vierge, un cran de molette de 60 px : scrollTop passe à 60, la largeur inline à 27.907%, la largeur rendue à 352,7 px sur une piste de 1 264 px — la proportion exacte, stabilisée sous 150 ms. Fin de course : 215 / 215, soit 100% ; retour en haut : 0%. Rien n'est calculé au chargement : tant qu'aucun événement scroll n'est survenu, style.width reste vide et la barre à 0 %.

Le pourcentage n'existe que comme largeur. La variable progress vaut bien 0 à 100, mais le code ne l'écrit nulle part en texte : le lecteur voit une barre, pas « 47 % ». L'afficher tient en une ligne à la suite de l'existante — label.textContent = Math.round(progress) + ' %' — et c'est ce que font les trois scènes ci-dessous. Trois choix d'écriture, vérifiés sur une page vierge. Le cadre .st-progress-scroll livre son propre fond : collé tel quel sur une page blanche, le bloc reste un rectangle sombre où titres, texte et piste se lisent (mesuré : aucun bloc de texte de la couleur du fond, sur page blanche comme sur page sombre) — sans ce fond, un texte blanc n'existerait que sur un site déjà sombre. Le même cadre est le repère du libellé : un élément en position: absolute se cale sur son ancêtre positionné le plus proche, et le cadre l'est ; le libellé tombe à 8 px du bas du bloc, sur la dernière ligne visible (mesuré : y = 268 dans un cadre de 280). Enfin le cadre est une colonne flex à max-height: 100% : posé dans une boîte de hauteur fixe plus basse que 280 px, le conteneur se réduit à la boîte au lieu de déborder (mesuré : dans une boîte de 250 px, conteneur à 250 px, piste et libellé visibles) ; sur une page sans contrainte de hauteur, ce max-height est sans effet. Le code vise en revanche deux id fixes : une seconde copie du bloc sur la même page a une barre qui ne bouge jamais (mesuré : style.width vide sur la seconde après 100 px de défilement).

Accessibilité

  • prefers-reduced-motion absent du code. Ce qui bouge de lui-même est limité : la pulsation d'opacité du libellé (2 s, infinie — mesurée en cours d'exécution sous la préférence « réduire ») et le lissage de 50 ms de la barre, qui ne fait que suivre le geste. scroll-behavior: smooth, lui, anime tout défilement programmé ou par ancre (mesuré : scrollTop = 108 posé par script arrive en ~160 ms dans Chromium, au lieu d'un saut immédiat). Ajoutez @media (prefers-reduced-motion: reduce) { .scroll-container { scroll-behavior: auto } .scroll-hint-label { animation: none } } ; la barre elle-même n'est pas concernée, elle suit l'utilisateur.
  • Clavier : le conteneur n'a pas de tabindex. Chrome 130+ et Firefox rendent les zones défilantes sans enfant focalisable atteignables par Tab (mesuré dans Chromium 151 : Tab focalise #progress-scroll, Flèche bas défile de 40 px, barre à 18,6 %) ; Safari non. Posez tabindex="0", role="region" et un aria-label sur le conteneur pour que le clavier fonctionne partout et que la zone soit annoncée.
  • La barre est muette : deux div vides dont seule la largeur change, rien n'est annoncé. Pour une lecture attestée ou un module de cours, donnez à la piste role="progressbar", aria-valuemin="0", aria-valuemax="100" et mettez à jour aria-valuenow dans l'écouteur (une ligne, à côté de style.width) — ou affichez le pourcentage en texte dans un élément aria-live="polite". Si la barre reste décorative, aria-hidden="true" sur la piste.
  • Contraste : sur #0a0a0f, les titres blancs mesurent 19,8:1 et le texte à 50 % d'opacité 5,3:1 (AA) ; le libellé « Scrollez pour explorer » à 30 % d'opacité mesure 2,6:1 au repos et descend plus bas pendant la pulsation — décoratif, à ne pas rendre porteur d'information. La piste (blanc à 8 %) est sous le seuil 3:1 des composants : le remplissage coloré se voit, la part restante ne se devine pas ; montez-la à 20 % ou affichez le chiffre. Ces rapports valent tels quels sur n'importe quelle page : le cadre .st-progress-scroll livre son fond #0a0a0f. Si vous passez le cadre en clair, changez dans le même temps les titres (blanc), le texte (blanc à 50 %) et la piste (blanc à 8 %), sinon tout disparaît.

Compatibilité navigateur

Trois propriétés fixent le plancher : la syntaxe à deux valeurs overflow: hidden auto (Chrome 68, Firefox 61, Safari 13.1 — avant, la déclaration est ignorée et le conteneur ne défile pas), position: sticky (Chrome 56, Firefox 32, Safari 13) et, pour le seul défilement programmé, scroll-behavior (Safari 15.4). Le JavaScript est en ES2015 (const, fonction fléchée) : Chrome 49, Firefox 45, Safari 10. Zéro dépendance. La progression est calculée en JavaScript sur l'événement scroll, pas par animation-timeline : elle fonctionne sur Firefox, qui ne rend les animations pilotées au défilement en CSS natif dans aucune version.

Chrome 68+✓ Complet
Firefox 61+✓ Complet (barre de défilement standard : ::-webkit-scrollbar ignoré)
Safari 13.1+✓ Complet (défilement programmé sans lissage avant 15.4)
Edge 79+✓ Complet
Mobile iOS 13.4+✓ Complet (barre de défilement système : les styles ::-webkit-scrollbar sont sans effet)
Android Chrome 68+✓ Complet

Sans JavaScript, le conteneur défile normalement et la piste reste vide : une ligne grise de 4 px, aucune erreur. Sans la syntaxe overflow à deux valeurs (Safari avant 13.1), le conteneur ne défile pas : le texte déborde des 280 px et la barre ne bouge jamais — écrivez overflow-x: hidden; overflow-y: auto en deux déclarations pour couvrir ces versions.

Le code

Copiez les trois blocs dans votre page. Aucune dépendance.

HTML
<div class="st-progress-scroll">
  <div class="scroll-container" id="progress-scroll">
    <div class="st-progress-bar-track">
      <div class="st-progress-bar-fill" id="progress-fill"></div>
    </div>
    <div class="progress-inner">
      <h3>Introduction</h3>
      <p>Le scrollytelling est une technique narrative qui utilise le defilement de la page pour guider l'utilisateur a travers une histoire visuelle immersive.</p>
      <div class="section-divider"></div>
      <h3>Chapitre 1</h3>
      <p>Les animations declenchees par le scroll permettent de reveler progressivement le contenu, creant un rythme naturel de lecture qui captive l'attention.</p>
      <div class="section-divider"></div>
      <h3>Chapitre 2</h3>
      <p>En combinant parallaxe, transitions et effets visuels, chaque section devient une scene dans un recit plus large, transformant un simple site web en experience narrative.</p>
      <div class="section-divider"></div>
      <h3>Chapitre 3</h3>
      <p>Les meilleurs exemples de scrollytelling equilibrent esthetique et fonctionnalite, guidant l'utilisateur sans le submerger d'effets excessifs.</p>
      <div class="section-divider"></div>
      <h3>Conclusion</h3>
      <p>Maitrisez ces techniques pour creer des experiences web memorables qui racontent votre histoire avec elegance et fluidite.</p>
    </div>
  </div>
  <span class="scroll-hint-label">Scrollez pour explorer</span>
</div>
CSS
/* Cadre de l'effet : il porte le fond, la couleur du texte et la police (remplacez-les par les vôtres),
   sert de repère (position: relative) au libellé placé en absolu, et, posé dans une boîte de hauteur
   fixe, réduit le conteneur au lieu de le laisser déborder (colonne flex + max-height: 100%). */
.st-progress-scroll {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-height: 100%;
  background: #0a0a0f;
  color: #fff;
  font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

.scroll-container {
  width: 100%;
  height: 280px;
  overflow: hidden auto;
  position: relative;
  scroll-behavior: smooth;
}

.scroll-container::-webkit-scrollbar {
  width: 4px;
}

.scroll-container::-webkit-scrollbar-track {
  background: rgba(255, 255, 255, 0.03);
}

.scroll-container::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.15);
  border-radius: 4px;
}

.scroll-container::-webkit-scrollbar-thumb:hover {
  background: rgba(255, 255, 255, 0.3);
}

.scroll-hint-label {
  position: absolute;
  bottom: 8px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.65rem;
  color: rgba(255, 255, 255, 0.3);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  pointer-events: none;
  z-index: 10;
  animation: pulseHint 2s ease-in-out infinite;
}

@keyframes pulseHint {
  0%, 100% {
    opacity: 0.3;
  }
  50% {
    opacity: 0.7;
  }
}

.st-progress-bar-track {
  position: sticky;
  top: 0;
  left: 0;
  width: 100%;
  height: 4px;
  background: rgba(255, 255, 255, 0.08);
  z-index: 20;
}

.st-progress-bar-fill {
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, #6366f1, #a855f7, #ec4899);
  border-radius: 0 2px 2px 0;
  transition: width 0.05s linear;
}

.progress-inner {
  padding: 24px 20px;
}

.progress-inner h3 {
  font-size: 1rem;
  font-weight: 700;
  color: #fff;
  margin: 0 0 12px;
}

.progress-inner p {
  font-size: 0.8rem;
  color: rgba(255, 255, 255, 0.5);
  line-height: 1.7;
  margin: 0 0 16px;
}

.progress-inner .section-divider {
  width: 60px;
  height: 2px;
  background: linear-gradient(90deg, #6366f1, transparent);
  margin: 20px 0;
}
JavaScript (fx-0575)
(function () {
  const progressScroll = document.getElementById('progress-scroll');
  const progressFill = document.getElementById('progress-fill');

  if (progressScroll && progressFill) {
    progressScroll.addEventListener('scroll', () => {
      const scrollTop = progressScroll.scrollTop;
      const scrollHeight = progressScroll.scrollHeight - progressScroll.clientHeight;
      const progress = scrollHeight > 0 ? (scrollTop / scrollHeight) * 100 : 0;
      progressFill.style.width = progress + '%';
    });
  }
})();

Personnaliser

Options passées à l'API ou attributs data-* :

Option / propriétéDéfautEffet
Hauteur du conteneur (CSS — .scroll-container height) 280px Fixe la fenêtre de lecture, donc la course : scrollHeight − 280. Plus le conteneur est haut, moins il y a à défiler et plus la barre avance par cran. Dans une colonne flex, remplacez par flex: 1; min-height: 0 — c'est ce que font les scènes. Posé dans une boîte plus basse, le cadre (colonne flex, max-height: 100%) réduit le conteneur à la boîte sans toucher à cette valeur.
Fond, couleur et police (CSS — .st-progress-scroll) #0a0a0f, blanc, Inter puis police système Une seule règle habille le bloc : c'est là que vont vos couleurs et votre police. Le fond est nécessaire, titres, texte et piste sont blancs. Pour un cadre clair, changez aussi .progress-inner h3, .progress-inner p et .st-progress-bar-track. Retirez le fond si le bloc est posé sur un site déjà sombre : la couleur et la police restent utiles.
Épaisseur de la piste (CSS — .st-progress-bar-track height) 4px Filet collé en haut du conteneur. 2 px = trait discret ; 8 px et plus = barre lisible de loin, augmentez alors le border-radius du remplissage (2 px) en proportion.
Couleur du remplissage (CSS — .st-progress-bar-fill background) linear-gradient(90deg, #6366f1, #a855f7, #ec4899) Le dégradé est tracé sur la largeur courante du remplissage : à 25 %, on ne voit que l'indigo et le violet ; le rose n'apparaît qu'en fin de course. Une couleur unie donne une lecture plus neutre pour un document ; posez le dégradé sur la piste et un masque sur le remplissage si vous voulez des teintes fixes.
Lissage (CSS — .st-progress-bar-fill transition) width 0.05s linear 50 ms entre deux positions de la barre. 0 = rendu brut à chaque événement scroll ; 0.2s = barre qui rattrape le geste, à ne pas dépasser (elle paraît en retard).
Défilement programmé (CSS — .scroll-container scroll-behavior) smooth N'affecte ni la molette ni le doigt : seulement scrollTop = x, scrollTo() et les ancres, animés en ~600 ms mesurés. Passez à auto si un script doit positionner le conteneur sans animation — ou posez scroll-behavior: auto en inline le temps de l'affectation.
Affichage du pourcentage (JS — à ajouter après style.width) absent label.textContent = Math.round(progress) + ' %' dans l'écouteur : le chiffre existe déjà dans la variable progress. C'est l'ajout qui fait de la barre un indicateur de lecture et non une décoration.
Identifiants (HTML/JS — progress-scroll, progress-fill) un seul exemplaire getElementById lie une seule instance. Pour plusieurs conteneurs, remplacez les deux id par querySelectorAll('.scroll-container') et, pour chacun, cherchez son .st-progress-bar-fill avant d'attacher l'écouteur — la formule ne change pas.
Libellé d'invite (HTML/CSS — .scroll-hint-label) « Scrollez pour explorer », pulsation 2 s Élément absolu, calé à 8 px du bas du cadre .st-progress-scroll (son repère position: relative) : il recouvre la dernière ligne visible du conteneur. Supprimez-le sans risque, le JS ne le lit pas ; sinon changez le texte et coupez la pulsation sous prefers-reduced-motion.

FAQ

Pourquoi cette barre plutôt que Scroll Progress Beam (fx-0793), qui suit aussi le défilement ?

Parce qu'ici la progression est un nombre que vous pouvez lire et exploiter. Le code calcule progress de 0 à 100 dans un écouteur scroll de quatre lignes, sur un conteneur défilant : vous pouvez l'écrire en texte, le comparer à un seuil (activer un bouton à 100 %), l'envoyer à votre outil de mesure. Le Beam est un indicateur lumineux latéral, décoratif, sans valeur explicite, pensé pour la page entière. L'un comme l'autre restent en JavaScript classique, donc disponibles sur Firefox — à la différence des barres construites sur animation-timeline: scroll(), que Firefox ne rend dans aucune version.

La barre reste à 0 % alors que mon conteneur n'est pas en haut : pourquoi ?

Le script ne calcule rien au chargement, il attend le premier événement scroll. Si vous positionnez le conteneur par script (scrollTop = 600 pour une reprise de lecture) ou si le navigateur restaure une position, l'événement est émis et la barre suit — mesuré : scrollTop = 108 posé par script donne 50.2% après 650 ms, le scroll-behavior: smooth ayant fait le trajet en ~160 ms. En revanche, un contenu injecté, une image qui charge ou un redimensionnement changent scrollHeight sans émettre scroll : la barre garde l'ancienne proportion. Sortez le corps de l'écouteur dans une fonction update(), appelez-la à l'initialisation et sur resize.

Comment mettre plusieurs documents, chacun avec sa barre, sur la même page ?

Le code vendu lie un seul exemplaire par getElementById : avec deux copies du bloc, la seconde a une barre qui ne bouge jamais (mesuré : style.width vide après 100 px de défilement), et deux id identiques sont de toute façon un HTML invalide. Retirez les id et parcourez document.querySelectorAll('.scroll-container') : pour chaque conteneur, box.querySelector('.st-progress-bar-fill') donne sa barre, et l'écouteur est le même. Les trois scènes de cette page fonctionnent ainsi, chacune sur son propre conteneur.