Scroll✨ Premium

Snap + Progress

Cinq sections de 280 px en scroll-snap mandatory dans un conteneur de 280 px ; à chaque événement scroll, le JS écrit la largeur d'une barre de 3 px (0, 25, 50, 75, 100 % aux cinq paliers) et active le point d'index Math.round(scrollTop / 280) ; un clic sur l'un des cinq points appelle scrollTo({ behavior: 'smooth' }). 1,3 Ko de JS, aucune boucle, aucun écouteur global.

JSSnapProgress

Vous aurez accès à la démo interactive avec un compte gratuit.

Cet effet fait partie d'Effect.Labs — 811 effets vanilla, une partie gratuite, une partie premium. Scroll comprend 70 effets, dont 12 gratuits. Découvrir la catégorie →

3 exemples d'usage

Certification sécurité incendie — cinq modules obligatoires suivis à la barre — Snap + Progress exemple 1

① Certification sécurité incendie — cinq modules obligatoires suivis à la barre

QuandFormation CPF réglementaire (sécurité incendie, cinq modules) suivie sur un intranet ou une plateforme e-learning : chaque module est une section, l'attestation dépend d'un parcours complet.
PourquoiUne formation réglementaire exige deux informations à la fois : où j'en suis dans le parcours (la barre : 0, 25, 50, 75, 100 %) et quel module est affiché (le point actif), avec retour possible sur un module déjà lu — un clic sur le point 2 y ramène. fx-0584 n'a que les points et ne mesure rien globalement ; fx-0593, CSS pur, n'offre aucun repère de complétion. Un salarié rendu au module 3 lit 50 % : le code mesure la position dans la course (scrollTop / 1120), pas le nombre de modules validés, et les points signalent le module ouvert, pas les modules terminés — ce statut vient de la plateforme, pas de l'effet.
RéglagesWrapper, conteneur et sections à la hauteur du lecteur (les trois sont couplés, voir Personnaliser) ; points portés à 12 px avec aria-label « Module n sur 5 » ; barre à 4 px. Pour afficher un pourcentage de modules validés plutôt qu'une position, remplacer scrollTop / scrollHeight par (currentSection + 1) / dots.length dans l'écouteur.
Questionnaire de satisfaction après livraison — cinq questions, une par écran — Snap + Progress exemple 2

② Questionnaire de satisfaction après livraison — cinq questions, une par écran

QuandCourriel post-achat ouvrant une page de cinq questions (délai, emballage, conformité, recommandation, commentaire) : une question par section, envoi à la fin.
PourquoiVoir combien de questions restent réduit l'abandon : la barre monte par paliers de 25 % et les points ramènent en un clic sur une réponse à corriger avant l'envoi. fx-0598, le voisin lissé, observe l'entrée de chaque section avec IntersectionObserver mais n'affiche aucun indicateur : le répondant ne sait ni où il en est ni combien il lui reste.
RéglagesFond clair : dégradés remplacés par #f8fafc, texte #0f172a, piste rgba(15,23,42,.08), points de 10 px en rgba(15,23,42,.18), actif #6366f1 (4,5:1 sur blanc), colonne centrée verticalement (top: 50%; transform: translateY(-50%)) ; scroll-snap-stop: always sur les sections pour interdire de sauter une question d'un coup de molette.
Bilan de santé annuel — cinq sections et un accès direct à la synthèse — Snap + Progress exemple 3

③ Bilan de santé annuel — cinq sections et un accès direct à la synthèse

QuandEspace patient d'un laboratoire ou d'un centre de santé : résultats présentés en cinq sections (Cardio, Métabolisme, Endocrinologie, Ossature, Synthèse), consultés surtout sur téléphone.
PourquoiLe patient pressé veut la synthèse d'abord : le cinquième point l'y amène en un clic (1120 px lissés en ≈ 450 ms, mesurés dans Chromium), et la barre lui dit ensuite s'il a tout parcouru. fx-0596 (hauteurs variables) n'a aucune navigation directe ; fx-0593 non plus. Cet accès direct n'existe au clavier et au lecteur d'écran que si les points deviennent des boutons nommés (« Synthèse ») — le code livré ne le fait pas.
RéglagesPoints en <button aria-label> portant le titre de la section, celui de la synthèse à 12 px pour signaler sa fonction ; remplissage #14b8a6 → #2dd4bf et point actif #2dd4bf sur fond ardoise #0b1220 (9,3:1) ; valeurs en font-variant-numeric: tabular-nums.

Comment ça marche

.sn-progress-wrapper (position: relative, 100 % × 280 px) contient trois choses. La piste .sn-progress-bar-track (absolue en haut, 3 px, blanc à 10 %, z-index: 5) avec son remplissage #snProgressBarFill : dégradé 90° #6366f1 → #d946ef, width: 0%, transition: width .3s. La colonne #snProgressDots (absolue, top: 10px; right: 8px, flex vertical, gap: 6px) : cinq div.sn-progress-dot de 8 px, blanc à 20 %, cursor: pointer, transition: .3s ; la classe .active les passe en #8b5cf6 et scale(1.3) (10,4 px mesurés). Et le défileur #snProgressContainer (100 % × 280 px, overflow-y: auto, scroll-snap-type: y mandatory, scroll-behavior: smooth) qui empile cinq .sn-progress-section de 280 px en scroll-snap-align: start, flex centré, padding: 20px, chacune avec son dégradé à 135° (#1e1b4b → #312e81 pour la première, #7c3aed → #8b5cf6 pour la dernière), un h4 de 0,9 rem (avec un font-family: Inter… en ligne dans le HTML), un p de 0,72 rem en blanc à 60 % et un numéro .sn-section-number de 3 rem à 8 % de blanc en filigrane. Mesuré : scrollHeight 1400, clientHeight 280, course de 1120 px. Le prélude .demo-preview qui enveloppe le tout apporte le box-sizing: border-box de tous les descendants et l'indication « Scrollez ↓ » qui pulse en 2 s.

Un seul écouteur scroll sur le conteneur fait tout le travail, sans requestAnimationFrame ni IntersectionObserver. Il calcule progress = scrollTop / (scrollHeight − clientHeight) × 100 et l'écrit dans barFill.style.width ; puis currentSection = Math.round(scrollTop / clientHeight) et bascule .active sur le point de cet index. Mesuré aux cinq positions de repos : 0, 25, 50, 75 et 100 % — le pourcentage est une position dans la course, pas un compte de sections : la première section lit 0 % et la dernière seule atteint 100 %. Le point, lui, bascule à mi-section (139 px → point 1, 141 px → point 2). Comme la barre a une transition de 0,3 s, elle est en retard sur la valeur : 80 ms après un coup de molette, le style dit 51 % quand la largeur rendue mesure 28 % (209,7 px sur 744).

L'accroche des sections est faite par le CSS (mandatory), pas par le JS. Mesuré dans Chromium 151 : un coup de molette de 60 px déplace de 42 px puis ramène à 0 (la barre affiche 3,75 % puis 0 %) ; 120 px → 85 px puis retour ; 200 px → 280 px, la deuxième section ; 300 px = exactement une section par coup ; au-delà de la fin, 1120 px et 100 % restent. Un clic sur un point appelle container.scrollTo({ top: index × clientHeight, behavior: 'smooth' }) : le lissage est celui du navigateur, mesuré sur 840 px (point 1 → point 4) : 27, 192, 505, 671, 744, 789, 817, 834, 840 px à 50 ms d'intervalle, soit ≈ 450 ms ; 280 px en ≈ 400 ms. Les événements scroll émis pendant ce lissage font suivre barre et points en continu. Les seuls écouteurs sont ceux du conteneur et des cinq points : ils disparaissent avec le DOM, rien n'est posé sur window. Le CSS vendu contient en revanche 18 règles étrangères à l'effet (.customizer-panel, .code-tab, .favorite-btn…), 1,5 Ko sur 4,2, qui ne ciblent rien.

Accessibilité

  • prefers-reduced-motion absent du code. Mesuré sous l'émulation reduce : scroll-behavior reste smooth (clic sur le point 5 : 1120 px lissés en ≈ 550 ms), les transitions de 0,3 s restent, la pulsation « Scrollez ↓ » tourne (animation-play-state: running). Ajoutez @media (prefers-reduced-motion: reduce) { .sn-progress-snap-container { scroll-behavior: auto } .sn-progress-bar-fill, .sn-progress-dot { transition: none } .sdc-scroll-hint { animation: none } } ; le behavior: 'smooth' du JS doit aussi devenir 'auto' sous cette préférence, sinon il force le lissage malgré le CSS.
  • Les cinq points sont des div vides : ni role, ni nom, ni tabindexfocus() refusé (mesuré), invisibles au clavier et au lecteur d'écran, et une cible de 8 px (10,4 px active) loin des 24 px minimum de WCAG 2.5.8. Remplacez-les par <button type="button" class="sn-progress-dot" data-index="1" aria-label="Section 2 sur 5"> dans un <nav>, posez aria-current="true" en même temps que .active et agrandissez la zone cliquable par un padding : le JS, qui lit data-index et bascule une classe, fonctionne sans changement.
  • Le défilement clavier fonctionne, par le conteneur. Mesuré dans Chromium 151 : Tab atteint le conteneur (défileur sans enfant focalisable), et après un clic dans une section, ↓, Page suivante, Espace, Fin et Origine avancent exactement d'une section (280, 560, 840, 1120, 0 px), barre et points suivent. Sur les navigateurs plus anciens, posez tabindex="0" sur .sn-progress-snap-container avec un anneau de focus visible. La barre est un div muet : donnez-lui role="progressbar" avec aria-valuenow mis à jour dans l'écouteur, ou aria-hidden="true" et un texte masqué « Section 2 sur 5 » actualisé au même endroit.
  • Contraste mesuré sur les pixels rendus. Point actif #8b5cf6 : 2,97:1 sur la section 1, 1,87:1 sur la 3, 1,07:1 sur la 5 — le fond y est #8854f4, même teinte : le repère de position disparaît sur la dernière section ; points inactifs de 1,4 à 1,8:1 partout. Le remplissage de la barre à 100 % passe sur la section 5 à 1,05:1 (#6366f1) et 1,22:1 (#d946ef), calculés. Textes : h4 blanc de 16:1 à 4,2:1 (section 5, 14,4 px gras : sous 4,5:1) ; p à 60 % de blanc 6,5:1 sur la première, 3,5:1 dès la quatrième, 2,5:1 sur la dernière. Un point actif blanc (≥ 4,2:1 sur les cinq fonds), un liseré de 1 px sur les inactifs et un p à 85 % règlent l'essentiel.

Compatibilité navigateur

Code ES5 (var, function, forEach) ; requiert scroll-snap-type / scroll-snap-align (spécification finale), scroll-behavior: smooth, Element.scrollTo() avec objet d'options, classList.toggle(nom, force) et linear-gradient. Ni IntersectionObserver, ni requestAnimationFrame, ni animation-timeline : barre et points sont pilotés par l'événement scroll classique, ce qui inclut Firefox, qui ne supporte les animations pilotées au défilement en CSS natif dans aucune version. Zéro dépendance.

Chrome 69+✓ Complet
Firefox 68+✓ Complet
Safari 15.4+✓ Complet (11 à 15.3 : accroche OK, clic sur un point sans lissage)
Edge 79+✓ Complet
Mobile iOS✓ Complet (points de 8 px difficiles au doigt)
Android Chrome✓ Complet (idem)

Sans JavaScript, l'accroche et le défilement lissé restent (CSS) mais la barre reste à 0 %, le premier point reste actif (état écrit dans le HTML) et les points ne font rien. Sans scroll-snap (navigateurs antérieurs à 2018), le conteneur défile librement ; barre et points suivent quand même, puisque le JS ne lit que scrollTop, le point basculant à mi-section. Safari 11 à 15.3 ignore scroll-behavior : le clic sur un point saute d'un coup à la section.

Le code

Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :

index.html — structure
<div class="sn-progress-wrapper">
  <div class="sn-progress-bar-track">
    <div class="sn-progress-bar-fill" id="snProgressBarFill"></div>
  </div>
  <div class="sn-progress-dots" id="snProgressDots">
    <div class="sn-progress-dot active" data-index="0"></div>
    <div class="sn-progress-dot" data-index="1"></div>
    <div class="sn-progress-dot" data-index="2"></div>
    <div class="sn-progress-dot" data-index="3"></div>
    <div class="sn-progress-dot" data-index="4"></div>
  </div>
  <div class="sn-progress-snap-container" id="snProgressContainer">
    <div class="sn-progress-section">
      <h4>Etape 1</h4>
      <p>Commencez votre parcours</p>
      <span class="sn-section-number">01</span>
    </div>
    <div class="sn-progress-section">…</div>
    <div class="sn-progress-section">…</div>
    <div class="sn-progress-section">…</div>
    <div class="sn-progress-section">
      <h4>Etape 5</h4>
      <p>Mission accomplie!</p>
      <span class="sn-section-number">05</span>
    </div>
  </div>
</div>
🔒 Débloquer le code complet — à partir de 2,99 € le 1er mois

HTML + CSS + JS complet, prêt à coller — avec des centaines d'effets premium.

Personnaliser

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

Option / propriétéDéfautEffet
Hauteur des sections (CSS — .sn-progress-wrapper, .sn-progress-snap-container, .sn-progress-section) 280 px, trois fois Le JS prend container.clientHeight pour hauteur de section : les trois valeurs doivent rester égales. Mesuré avec un conteneur de 400 px sur des sections de 280 : barre à 28 / 56 / 84 / 100 %, point 2 encore actif sur la section 3, clic sur le point 5 arrêté à 1000 px sur le point 4. 100vh sur les trois donne des sections plein écran.
Nombre de sections (HTML — .sn-progress-section et .sn-progress-dot[data-index]) 5 et 5 Une section ajoutée = un point ajouté avec l'index suivant, sinon Math.round désigne un point inexistant. Les paliers de la barre deviennent 100 / (n − 1) : six sections donnent 0, 20, 40, 60, 80, 100 %. Prévoir la règle :nth-child(6) du fond.
Accroche (CSS — scroll-snap-type: y mandatory) mandatory proximity autorise un repos entre deux sections ; le point bascule quand même à mi-hauteur. Ajoutez scroll-snap-stop: always sur les sections pour interdire d'en sauter une : mesuré depuis le haut, un coup de molette de 600 px saute la section 2 pour se poser sur la 3 (560 px), et 1000 px vont d'un trait en fin de course (1120 px).
Barre (CSS — .sn-progress-bar-fill linear-gradient(90deg, #6366f1, #d946ef), .sn-progress-bar-track height: 3px) indigo → magenta, 3 px Le dégradé est peint sur l'élément puis rogné par sa largeur : à 25 % on ne voit que le premier quart, le magenta n'apparaît qu'aux abords de 100 %. Pour une couleur constante, une couleur unie ; pour un dégradé complet à toute largeur, background-size sur la piste plutôt que sur le remplissage.
Retard de la barre (CSS — transition: width .3s) 0,3 s Mesuré : 80 ms après un palier, le style annonce 51 % et la largeur rendue 28 %. 0s pour une barre collée au doigt sur tactile ; au-delà de 0,5 s elle finit son mouvement après l'accroche.
Points (CSS — .sn-progress-dot 8 px, rgba(255,255,255,.2) ; .active #8b5cf6 + scale(1.3) ; .sn-progress-dots top: 10px; right: 8px; gap: 6px) 8 px, colonne en haut à droite 12 px et un padding de 6 px font une cible de 24 px ; top: 50%; transform: translateY(-50%) centre la colonne. Le point actif #8b5cf6 mesure 1,07:1 sur le fond de la cinquième section : passez-le en blanc, ou changez le dernier dégradé.
Clic sur un point (JS — behavior: 'smooth') smooth 'auto' saute d'un coup. La durée du lissage est celle du navigateur (≈ 450 ms pour 840 px mesurés dans Chromium) et ne se règle pas ; pour une durée choisie, animez scrollTop vous-même en requestAnimationFrame et passez scroll-behavior: auto en CSS, sinon les deux lissages se superposent.
Fonds des sections (CSS — .sn-progress-section:nth-child(1…5)) cinq dégradés à 135°, de #1e1b4b à #8b5cf6 Chaque section a sa règle ; un fond commun sur .sn-progress-section suffit si les cinq ne doivent pas différer. Les deux derniers dégradés font tomber le p à 60 % sous 3,5:1.

FAQ

Parce que progress = scrollTop / (scrollHeight − clientHeight) mesure la position dans la course : cinq sections font quatre intervalles, donc des paliers de 25 %, et la première section est à 0 puisque rien n'a encore défilé. Pour afficher « 1 sur 5 = 20 % », calculez plutôt (currentSection + 1) / dots.length × 100 dans l'écouteur, juste après currentSection ; la barre basculera alors à mi-section, comme les points, au lieu de glisser en continu.
Même accroche scroll-snap-type: y mandatory (fx-0598 y ajoute scroll-snap-stop: always), mais pas le même JS. fx-0598 observe chaque section avec un IntersectionObserver (root = le conteneur, threshold: 0.6) et pose .in-view sur celle qui entre : un événement par section, fait pour animer son contenu, sans aucun indicateur affiché. fx-0597 écoute scroll et en tire deux repères — le pourcentage global et l'index de la section — plus une navigation par clic. Scroll Snap Sections (fx-0584) n'a que les points et divise par la constante 280 au lieu de clientHeight. Les deux mécanismes se combinent sur un même conteneur : l'observateur pour révéler le contenu, cet écouteur pour la barre et les points.
Le box-sizing: border-box vient du prélude .demo-preview *, pas du CSS des sections, qui ont height: 280px et padding: 20px. Mesuré sans le prélude : sections de 320 px dans une fenêtre de 280, scrollHeight 1600, barre à 24,2 / 48,5 / 72,7 / 97 %, aucun point actif à 1280 px (Math.round(1280 / 280) = 5, sixième point inexistant), et le clic sur le point 5 s'arrête à 1000 px. Ajoutez box-sizing: border-box à .sn-progress-section. Même origine pour la police : le h4 porte un font-family: Inter… en ligne, le p hérite de la page (Times, mesuré sur une page vierge).