Snap Gallery with Progress
Cinq écrans en accroche y mandatory et un rail de 3 px rempli par une timeline de défilement nommée : 0 → 100 % de la course, continu entre deux écrans, zéro JavaScript.
Mis à jour le
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
Le HTML vendu : le cadre .demo-preview (fond #0a0a0f, min-height: 300px), puis .snap-gallery-wrapper — 100% × 300 px, position: relative, timeline-scope: --snapScroll — qui contient le rail .snap-progress-bar (absolu, à 10 px du bord gauche, 3 px de large, piste blanche à 10 %) et sa jauge .snap-progress-fill (dégradé #6366f1 → #d946ef, height: 0%), la colonne .snap-progress de cinq .snap-dot (8 px, à 10 px du bord droit) et la zone .snap-gallery-container : 100% × 100%, overflow: hidden auto, scroll-snap-type: y mandatory, scroll-timeline: --snapScroll y. Dedans, cinq .snap-slide de 300 px en scroll-snap-align: start, un dégradé à 135° chacune, un numéro de 48 px à 15 % d'opacité et un libellé de 13,6 px. Sous le cadre, l'indice .slr-scroll-hint « Scrollez entre les slides » rebondit de 4 px toutes les 2 s. Champ JS vide ; la règle .snap-dots ne correspond à aucun élément. Mesuré en page vierge : 1 200 px de course, aucune erreur console, aucune ressource externe.
La jauge ne lit jamais scrollTop : c'est l'animation @keyframes snapProgressGrow (height: 0% → 100%, linear, animation-fill-mode: both) dont l'horloge est remplacée par la position de défilement. scroll-timeline: --snapScroll y déclare sur la zone une timeline nommée (0 en haut, 1 au dernier pixel de course) ; animation-timeline: --snapScroll y abonne la jauge, animation-duration: auto laisse la durée à la timeline et animation-range: normal couvre toute la course. Une timeline nommée n'est visible que de l'élément qui la déclare et de ses descendants ; la jauge est dans une branche sœur, et c'est timeline-scope: --snapScroll sur le cadre commun qui la met à sa portée. Sans cette ligne, mesuré dans Chromium 151, l'animation retombe sur l'horloge du document (durée 0 s, finished) et la jauge reste pleine. Avec : 0, 75, 150, 225, 300 px de jauge aux cinq écrans (scrollTop 0, 300, 600, 900, 1 200), à 1 280 comme à 390 px de large, et une valeur proportionnelle entre deux — 74 px à 296 px de défilement dans WebKit : le rail glisse pendant l'accroche elle-même.
L'accroche est celle du moteur : mandatory interdit tout repos entre deux écrans. Mesuré dans Chromium, un cran de molette de 150 px revient à 0 et 160 px passe à l'écran 2 (il faut dépasser la moitié de 300 px d'un seul geste) ; 900 px va droit à l'écran 4, faute de scroll-snap-stop. Firefox 153 et WebKit 26.5 avancent d'un écran dès 100 px, Firefox jamais plus d'un par geste. Pas de scroll-behavior: smooth : scrollTop = 600 posé par script se relit 600 aussitôt. Les cinq points sont fixes — aucune classe active, aucun clic : ils comptent les écrans, le rail dit où l'on en est. La hauteur du cadre est écrite en pixels parce qu'un pourcentage exige un parent à hauteur définie : en height: 100% sous .demo-preview seul, la galerie mesurait 75 px, cinq bandes de 15 px.
Accessibilité
- prefers-reduced-motion absent du code : mesuré sous émulation, la jauge suit toujours le geste (75 px à l'écran 2) et l'indice rebondit encore (
running). La jauge ne bouge que par le geste du lecteur, elle est acceptable telle quelle ; le rebond de l'indice est permanent : ajoutez@media (prefers-reduced-motion: reduce) { .slr-scroll-hint { animation: none } }. L'accroche est une position d'arrêt, pas une animation. - Clavier : mesuré, Chromium 151 et Firefox 153 donnent le focus à la zone par Tab sans
tabindex; flèche bas = 300, Page suivante = 600, Fin = 1 200, la jauge suit. WebKit 26.5 ne focalise pas une zone défilante sanstabindex="0"(focus resté surbody, flèches sans effet) : ajoutez-le, avec unaria-labelet un style:focus-visible. Rail et points sont décoratifs :aria-hidden="true"sur.snap-progress-baret.snap-progress. - Contraste : le libellé blanc tient partout, 5,0:1 au pire (sur
#b45309, écran 4). Le numéro à 15 % vaut 1,25 à 1,58:1 : un filigrane, illisible par construction — s'il porte un sens, écrivez-le aussi en clair. L'indice à 40 % fait 3,8:1 sur#0a0a0f. Composants (seuil 3:1) : bordure des points 2,4 à 5,0:1 selon le fond, piste du rail 1,1 à 1,4:1, jauge#6366f13,6:1 sur l'indigo du premier écran mais 1,2:1 sur le bleu du dernier — foncez la piste sur un fond clair ou bleu. - Lecteurs d'écran : cinq
divde deuxspan, le numéro est lu (« zéro un ») avant le libellé ; masquez-le ou faites du libellé un titre si les écrans sont des sections. Rien ne change dans l'arbre d'accessibilité au défilement : la progression est purement visuelle. L'indice est en français en dur. Au doigt,overscroll-behavior: containsur la zone évite d'entraîner la page en bout de course.
Compatibilité navigateur
CSS seul, zéro dépendance : scroll-timeline nommée, animation-timeline, timeline-scope, animation-range, animation-duration: auto, scroll-snap-type. L'accroche existe partout depuis 2018 ; c'est la jauge qui fixe le plancher : animations pilotées par le défilement dans Chrome et Edge 115, et timeline-scope, indispensable ici, dans Chrome et Edge 116 ; Safari et iOS 26 (mesuré dans WebKit 26.5 : mêmes valeurs que Chromium). Firefox : aucune version. Mesuré dans Firefox 153 : CSS.supports('animation-timeline: scroll()') répond faux, la durée calculée vaut 0s, snapProgressGrow tourne sur l'horloge du document et se termine aussitôt (finished, fill-mode: both) : la jauge est pleine à 100 % dès l'ouverture et le reste, à chaque écran. Tout le reste est intact : cinq écrans, accroche (100 px de molette = écran suivant), points, libellés, numéros, aucune erreur. Un visiteur Firefox voit une galerie qui accroche écran par écran sous un rail plein. Chrome et Edge 115 ont la timeline mais pas timeline-scope : la jauge ne l'atteint pas — mesuré sans cette ligne dans Chromium 151, elle reste pleine.
Aucun repli @supports dans le code vendu : là où la timeline manque, rien n'est caché ni déplacé, la jauge est simplement pleine. Si un rail plein vous gêne plus qu'un rail vide, @supports not (animation-timeline: scroll()) { .snap-progress-fill { animation: none } } la laisse à height: 0%. Une vraie progression sur Firefox demande un écouteur scroll qui écrit la hauteur — c'est ce que fait Snap + Progress (fx-0597).
Le code
Structure HTML à coller dans votre page (le CSS + JS sont inclus avec Premium) :
<div class="demo-preview">
<div class="snap-gallery-wrapper">
<div class="snap-progress-bar">
<div class="snap-progress-fill"></div>
</div>
<div class="snap-progress">
<div class="snap-dot"></div>
<div class="snap-dot"></div>
<div class="snap-dot"></div>
<div class="snap-dot"></div>
<div class="snap-dot"></div>
</div>
<div class="snap-gallery-container">
<div class="snap-slide">
<span class="snap-slide-number">…</span>
<span class="snap-slide-label">…</span>
</div>
<div class="snap-slide">…</div>
<div class="snap-slide">…</div>
<div class="snap-slide">…</div>
<div class="snap-slide">…</div>
</div>
</div>
<span class="slr-scroll-hint">…</span>
</div>
Le code complet HTML + CSS + JS de Snap Gallery with Progress, prêt à coller — et les 811 effets. Ou 299 € une fois, accès à vie.
Personnaliser
Options passées à l'API ou attributs data-* :
| Option / propriété | Défaut | Effet |
|---|---|---|
Hauteur du cadre (CSS — .snap-gallery-wrapper { height: 300px }) |
300 px (zone et écrans à 100 %, course 1 200 px) | Zone et écrans sont à 100% et suivent. 100vh (100dvh sur mobile) pour un écran par vue, avec la même hauteur sur .demo-preview. Un pourcentage exige un parent à hauteur définie : mesuré en height: 100%, 75 px de galerie. |
Nombre d'écrans (HTML — .snap-slide, .snap-dot ; CSS — :nth-child(1) à (5)) |
5 écrans, 5 points, 5 dégradés | Un sixième écran n'a pas de fond (mesuré : background-image: none) : ajoutez sa règle et un .snap-dot, rien n'est généré. La jauge suit d'elle-même : animation-range: normal couvre toute la course, 20 % par écran au lieu de 25. |
Rail (CSS — .snap-progress-bar, .snap-progress-fill) |
gauche 10 px, 3 px de large, piste blanche à 10 %, jauge #6366f1 → #d946ef |
left: auto; right: 10px pour le passer à droite, width: 6px pour l'épaissir, une couleur unie à la place du dégradé. Il peut vivre n'importe où sous le cadre qui porte timeline-scope ; sorti de ce cadre, il retombe à 100 % fixe. |
Progression de la jauge (CSS — animation-timing-function, animation-range sur .snap-progress-fill) |
linear, normal (0 → 100 % de la course) | steps(4) fait sauter la jauge par écran au lieu de glisser : mesuré 0, 0, 75, 75, 150 px à 0, 150, 300, 450, 600 px de défilement. animation-range: 0% 80% la remplit dès le quatrième écran. |
Accroche (CSS — scroll-snap-type, scroll-snap-align ; scroll-snap-stop à ajouter) |
y mandatory / start, sans scroll-snap-stop ni scroll-behavior | y proximity autorise un repos entre deux écrans si l'un peut dépasser la hauteur. scroll-snap-stop: always sur .snap-slide interdit de sauter des écrans : mesuré sans, un cran de 900 px va de l'écran 1 au 4 dans Chromium. scroll-behavior: smooth sur la zone si un bouton ou une ancre la pilote. |
Points (CSS — .snap-progress, .snap-dot) |
5 × 8 px, blanc 30 %, bordure 50 %, à 10 px du bord droit | Décoratifs et fixes : aucun état actif, aucun clic. Un point qui s'allume ou qui saute à l'écran demande du JavaScript — c'est Snap + Progress (fx-0597). Retirez le bloc si le rail suffit, et la règle orpheline .snap-dots avec lui. |
Fonds, textes et indice (CSS — .snap-slide:nth-child(n), .snap-slide-number, .snap-slide-label, .slr-scroll-hint) |
5 dégradés à 135°, numéro 48 px à 15 %, libellé 13,6 px centré, indice français rebondissant | Chaque écran est une boîte flex centrée : flex-direction: column; align-items: flex-start et un padding pour une mise en page de fiche ; sur un fond clair, changez la couleur de .snap-slide. L'indice est accroché à .demo-preview : supprimez le span, @keyframes hintBounce part avec lui. |
FAQ
finished) ; Chrome/Edge 115 n'ont pas timeline-scope et la jauge n'atteint pas la timeline. La portée : une timeline nommée n'est visible que de l'élément qui la déclare (la zone) et de ses descendants ; la jauge est dans le rail, à côté, et ne la voit que grâce à timeline-scope: --snapScroll sur .snap-gallery-wrapper — mesuré sans, dans Chromium : 100 % à toute position. Si vous déplacez le rail, remontez timeline-scope sur l'ancêtre commun ; si vous renommez la timeline, renommez-la aux trois endroits. La course : sans hauteur définie ou sans contenu qui dépasse, rien ne défile et la jauge reste vide. Une timeline anonyme scroll() ne règle rien : elle prend le défileur ancêtre le plus proche de la jauge, la page, pas la zone.y mandatory, cinq sections d'une hauteur), tout le reste diffère. fx-0597 est en JavaScript : un écouteur scroll écrit la largeur d'une barre horizontale en haut, Math.round(scrollTop / hauteur) allume le point actif et un clic sur un point appelle scrollTo({ behavior: 'smooth' }) ; il tourne sur Firefox. fx-0574 n'a pas une ligne de script : rail vertical piloté par scroll-timeline / animation-timeline, points fixes, pas de clic ; Chrome/Edge 116+, Safari 26+, rail plein sur Firefox. fx-0574 quand le script n'est pas possible ou pas voulu (WebView verrouillée, CSP sans script en ligne, générateur statique) et que le public est sur Chrome ou Safari ; fx-0597 quand Firefox compte, quand les points doivent être cliquables ou quand la progression doit être lue par le reste de la page — un nombre en JavaScript, pas une hauteur en CSS..demo-preview seul (flex, min-height, pas de height), height: 100% vaut auto et la galerie mesurait 75 px, d'où les 300 px en dur. Pour un écran par vue, donnez au cadre height: 100vh (100dvh sur mobile) : zone et écrans à 100% suivent, la course devient quatre fois la hauteur et la jauge se recale seule. Gardez la zone comme défileur : c'est elle qui porte la timeline. Pour faire défiler la page elle-même, déplacez scroll-snap-type et scroll-timeline: --snapScroll y sur html, passez le rail en position: fixed (tout descend de html, timeline-scope devient inutile) et prévoyez scroll-padding-top sous un en-tête fixe. Dans tous les cas, proximity plutôt que mandatory dès qu'un écran peut dépasser la vue.