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.
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



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-behaviorrestesmooth(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 } }; lebehavior: '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
divvides : nirole, ni nom, nitabindex—focus()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>, posezaria-current="true"en même temps que.activeet agrandissez la zone cliquable par unpadding: le JS, qui litdata-indexet 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-containeravec un anneau de focus visible. La barre est undivmuet : donnez-luirole="progressbar"avecaria-valuenowmis à jour dans l'écouteur, ouaria-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 :h4blanc 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 unpà 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.
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) :
<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>
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éfaut | Effet |
|---|---|---|
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
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.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.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).