Scroll Fade Gallery
Cinq blocs 16/9 à 80 % de largeur dans un cadre de 280 px : chacun part de scale(.8) translateY(20px) et d'une opacité 0 et atteint sa taille pleine pendant toute son entrée — animation-timeline: view(), animation-range: entry 0% entry 100%, zéro JavaScript. Mesuré : un bloc plus court que le cadre finit à scale(1) exactement quand son bord bas touche le bas du cadre.
Mis à jour le
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
Albatros 38 — 2017
État des lieux d'entrée
12 rue des Lilas, Caen · 2 pièces, 46 m² · signé le 3 septembre 2026, 14 photos.
Rayure de 12 cm sur le parquet devant la fenêtre ; peinture des murs en bon état.
Trace de brûlure sur le plan de travail, à gauche des plaques ; carrelage et robinetterie en bon état.
Joint silicone noirci autour de la baignoire ; ventilation fonctionnelle.
Trou de cheville sur le mur gauche, à 1,60 m ; moquette propre, joints de fenêtre intacts.
Comment ça marche
Le HTML vendu tient en neuf éléments : le cadre .demo-preview (flex centré, height: 280px, overflow: hidden, position: relative, fond #0a0a0f, texte blanc, sans-serif), le défileur .fade-gallery-container (100% × 100%, overflow-y: auto, overflow-x: hidden, padding: 20px 12px), deux .scroll-spacer de 60 px qui encadrent cinq .fade-gallery-item vides — width: 80%, margin: 24px auto, aspect-ratio: 16/9, border-radius: 10px, cinq dégradés à 135° du rose au violet posés par :nth-child(2) à (6) (l'enfant 1 du défileur est l'espaceur) — et l'indice .slr-scroll-hint « Scrollez pour voir l'effet », absolu à 8 px du bas du cadre, 11,2 px, blanc à 40 %, qui rebondit de 4 px toutes les 2 s (hintBounce). Le champ JavaScript est vide. Mesuré dans Chromium 151, page vierge de 1 280 px avec une barre de défilement classique : cadre de 1 264 × 280, blocs de 992 × 558 px, pas de 582 px (les marges de 24 px fusionnent), course 2 814 px ; à 390 px : blocs de 280 × 158, course 812.
Le mouvement vient de trois déclarations sur le bloc. animation: fadeScaleIn linear both nomme les images-clés (from : opacité 0, scale(0.8) translateY(20px) ; to : opacité 1, scale(1) translateY(0)), sans durée, en progression linéaire, avec un remplissage qui tient l'état de départ avant la plage et l'état d'arrivée après. animation-timeline: view() remplace l'horloge par la position du bloc dans le défileur le plus proche — ici .fade-gallery-container, pas la page : une timeline de vue anonyme, rien à nommer ni à déclarer sur le défileur. animation-range: entry 0% entry 100% borne la course : 0 % quand le bord haut du bloc franchit le bas du cadre, 100 % quand le bloc a fini d'entrer. La progression étant linéaire, l'opacité vaut la fraction d'entrée, l'échelle 0,8 + 0,2 × fraction, et le translateY(20px), écrit après le scale(0.8) et donc multiplié par lui, fait 16 px à l'écran au départ. Mesuré à 390 px sur le bloc 2 (haut à 286 px du début du défileur, cadre de 280) : 0,346 et scale(0.869) après 60 px de molette, 0,727 / 0,945 à 120, 1 / 1 à 180 — la plage fait 158 px, la hauteur du bloc ; à mi-plage, 0,502, scale(0.9) et 9 px de décalage. Le bloc est net et entier au même instant.
« Fini d'entrer » a deux sens selon la taille du bloc. Plus court que le cadre, il a fini d'entrer quand son bord bas touche le bas du cadre : la plage vaut sa hauteur. Plus haut que le cadre, il ne peut jamais y tenir entier : la plage s'arrête quand il le remplit, bord haut contre bord haut, et vaut alors la hauteur du cadre. C'est le cas de la démo vendue sur un écran large — un 16/9 dépasse 280 px dès qu'il fait plus de 498 px de large, soit un cadre d'environ 650 px (mesuré : 279 px de haut à 660 px avec une barre de défilement classique, 297 à 700). À 1 280 px, le bloc 3 passe de 0 à scrollTop 988 à 0,996 à 1 267 : 280 px de course, puis il reste à 1 pendant que ses 278 px restants défilent. Tout est réversible, la timeline est une position, pas une horloge : remonter de 100 px en 100 px ramène le bloc 3 de 0,757 à 0,4, 0,043 puis 0 ; un bloc sorti par le haut reste à 1 (both).
Accessibilité
- prefers-reduced-motion absent du code : mesuré sous émulation, rien ne change — les blocs partent de 0 et grossissent à l'entrée, l'indice rebondit (
hintBouncemesurérunning). Le grossissement est piloté par le lecteur (WCAG 2.3.3 vise le mouvement non sollicité) et ne cache rien de plus qu'en temps normal, mais un bloc qui grossit sous le doigt reste un mouvement : ajoutez@media (prefers-reduced-motion: reduce) { .fade-gallery-item { animation: none; opacity: 1; transform: none } .slr-scroll-hint { animation: none } }— galerie à plat, comme dans Firefox. - Clavier : mesuré, Tab donne le focus au défileur sans
tabindexdans Chromium 151 comme dans Firefox 153 (activeElement=.fade-gallery-container, anneauoutline: auto 1px) ; flèche bas avance de 40 px (Chromium) ou 51 (Firefox), PageDown de 285 / 303, Fin va au bout (2 814). Safari ne focalise pas un bloc défilant sanstabindex="0": ajoutez-le, avec unaria-label(« Cinq visuels, défilement vertical ») et un style:focus-visible. - Lecteurs d'écran et images : les cinq blocs sont des
divvides à fond dégradé, rien à annoncer. L'opacité 0 n'est que visuelle : un bloc pas encore entré reste dans l'arbre d'accessibilité et dans l'ordre de lecture, aucun contenu n'est masqué au lecteur d'écran. Si vous y mettez des<img>, donnez-leur unalt; une légende posée dans le bloc suit son opacité (illisible tant qu'il n'est pas entré) — placez-la sous le bloc si elle doit se lire avant. - Indice et chaînage : « Scrollez pour voir l'effet » est écrit en français en dur, en 11,2 px et blanc à 40 % — rendu
rgb(108, 108, 111)sur#0a0a0f, mesuré 3,77:1, sous les 4,5:1 attendus pour ce corps ; posezaria-hidden="true"dessus, il est décoratif, ou retirez lespan. Au doigt, le geste doit commencer dans le cadre ; en bout de course, la molette entraîne la page (mesuré dans Chromium : 200 px de page après le dernier bloc) —overscroll-behavior: containsur le défileur l'évite.
Compatibilité navigateur
CSS seul : animation-timeline: view() et animation-range (animations pilotées par le défilement), aspect-ratio, overflow-y, linear-gradient, @keyframes. Zéro JavaScript, zéro dépendance, aucune variable CSS du site. Chrome et Edge 115+, Safari et iOS 26+ ; Firefox : aucune version — mesuré dans Firefox 153, CSS.supports('animation-timeline: view()') répond faux et la galerie s'affiche à plat.
Sans animations pilotées par le défilement (Firefox, Chrome avant 115, Safari avant 26), animation-timeline et animation-range sont ignorés ; le raccourci animation: fadeScaleIn linear both n'a pas de durée, donc 0 s, et le remplissage both applique l'état final : cinq blocs à opacité 1 et scale(1), le cadre défile normalement (mesuré Firefox 153 : course 2 814 px, identique, tous les blocs visibles). Rien n'est caché, c'est pourquoi le code n'a pas de bloc @supports not (animation-timeline: view()) — l'état final est déjà l'état lisible. Sans aspect-ratio (Chrome avant 88, Safari avant 15, Firefox avant 89), un div vide n'a pas de hauteur : la galerie disparaît, sauf à donner aux blocs une hauteur ou un contenu.
Le code
Copiez les trois blocs dans votre page. Aucune dépendance.
<div class="fade-gallery-container">
<div class="scroll-spacer"></div>
<div class="fade-gallery-item"></div>
<div class="fade-gallery-item"></div>
<div class="fade-gallery-item"></div>
<div class="fade-gallery-item"></div>
<div class="fade-gallery-item"></div>
<div class="scroll-spacer"></div>
</div>
<span class="slr-scroll-hint">Scrollez pour voir l'effet</span>
.slr-scroll-hint {
position: absolute;
bottom: 8px;
left: 50%;
transform: translateX(-50%);
font-size: 0.7rem;
color: rgba(255, 255, 255, 0.4);
pointer-events: none;
z-index: 10;
animation: hintBounce 2s ease-in-out infinite;
}
.scroll-spacer {
height: 60px;
}
.fade-gallery-container {
width: 100%;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
padding: 20px 12px;
}
.fade-gallery-item {
width: 80%;
margin: 24px auto;
aspect-ratio: 16/9;
border-radius: 10px;
opacity: 0;
transform: scale(0.8) translateY(20px);
animation: fadeScaleIn linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
/* Les fonds comptent les enfants du défileur : le premier enfant est l'espaceur,
les cinq blocs sont donc les enfants 2 à 6. */
.fade-gallery-item:nth-child(2) {
background: linear-gradient(135deg, #ec4899, #f43f5e);
}
.fade-gallery-item:nth-child(3) {
background: linear-gradient(135deg, #06b6d4, #3b82f6);
}
.fade-gallery-item:nth-child(4) {
background: linear-gradient(135deg, #10b981, #059669);
}
.fade-gallery-item:nth-child(5) {
background: linear-gradient(135deg, #f59e0b, #ea580c);
}
.fade-gallery-item:nth-child(6) {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
}
@keyframes hintBounce {
0%,
100% {
transform: translateX(-50%) translateY(0);
}
50% {
transform: translateX(-50%) translateY(-4px);
}
}
@keyframes fadeScaleIn {
from {
opacity: 0;
transform: scale(0.8) translateY(20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
Personnaliser
Options passées à l'API ou attributs data-* :
| Option / propriété | Défaut | Effet |
|---|---|---|
animation-range (CSS — .fade-gallery-item) |
entry 0% entry 100% | Le bloc atteint scale(1) exactement quand il a fini d'entrer. entry 0% entry 50% : fini à mi-entrée, la moitié basse encore sous le cadre (Scale In, fx-0536). entry 0% cover 50% : continue de grossir jusqu'à ce que le centre du bloc atteigne celui du cadre, donc après qu'il est entier (View Timeline Scale, fx-0546). entry 20% entry 80% resserre la course sur le milieu de l'entrée. |
Images-clés fadeScaleIn (CSS — @keyframes) |
opacité 0 → 1, scale(0.8) → scale(1), translateY(20px) → 0 | L'échelle de départ règle l'ampleur : scale(0.9) pour un grossissement discret, scale(0.6) pour un surgissement. Le translateY est multiplié par l'échelle (16 px rendus au départ) : écrivez-le avant le scale pour 20 px exacts, ou supprimez-le pour un pur zoom. Gardez le from identique au style de repos du bloc, sinon il saute avant d'entrer dans la plage. |
Largeur et ratio des blocs (CSS — width: 80%, aspect-ratio: 16/9) |
80 % du défileur, 16/9 (992 × 558 px à 1 280, 280 × 158 à 390) | La hauteur en découle, et avec elle la longueur de la plage : plus court que le cadre, le bloc finit à scale(1) entier ; plus haut, il finit quand il remplit le cadre. Dans un cadre de 280 px, un 16/9 à 80 % reste plus court jusqu'à ~650 px de large ; 4/3 ou 1/1 abaissent ce seuil, width: 60% le relève. |
Espacement (CSS — margin: 24px auto, .scroll-spacer { height: 60px }) |
24 px entre blocs (marges fusionnées), 60 px avant et après | L'espace avant le premier bloc décide de son état au repos (mesuré 0,629 à 1 280 px) : un premier espaceur au moins égal à la hauteur du cadre le fait partir de 0. Les marges règlent combien de blocs sont en transition en même temps : à 390 px, pas de 182 px, jamais plus d'un. |
Fonds des blocs (CSS — :nth-child(2) à (6), HTML) |
5 dégradés à 135°, du rose au violet | Le sélecteur compte les enfants du défileur, espaceur compris : les blocs sont les enfants 2 à 6. Un sixième bloc (enfant 7) n'a pas de fond, donc rien à voir sur le cadre sombre : ajoutez sa règle, ou remplacez les cinq par une seule couleur ou par des <img> (voir la dernière question). |
Progression (CSS — linear dans le raccourci animation) |
linear | Avec une timeline de défilement, la fonction de progression déforme le lien position → état, pas le temps : ease-out fait grossir vite au début de l'entrée et lentement à la fin, ease-in l'inverse. linear est le seul réglage où l'opacité vaut exactement la fraction d'entrée (mesuré 0,502 à mi-plage). |
Indice « Scrollez pour voir l'effet » (CSS — .slr-scroll-hint, hintBounce) |
8 px du bas du cadre, 11,2 px, blanc à 40 %, rebond de 4 px toutes les 2 s | Ancré au cadre .demo-preview (position: relative, mesuré offsetParent) : sans ce cadre, il tombe au bas de la page. Texte français en dur. Pour le retirer, supprimez le span ; la règle et la @keyframes hintBounce partent avec lui. |
FAQ
Pourquoi le premier bloc est-il déjà à moitié visible avant tout défilement sur un grand écran ?
Parce que sa plage a commencé avant le repos. La plage entry part quand le bord haut du bloc est au bas du cadre ; le premier bloc a son bord haut à 104 px du début (20 px de padding, 60 px d'espaceur, 24 px de marge), soit 176 px au-dessus de cette ligne dans un cadre de 280 px. À 1 280 px, le bloc fait 558 px de haut, sa plage vaut donc 280 px et 176 px sont déjà consommés : mesuré 0,629 et scale(0.926) avant tout geste. À 390 px, le bloc fait 158 px : 104 + 158 = 262 < 280, il est entier et sa plage est finie, mesuré 1. Pour qu'il parte toujours de 0, donnez au premier .scroll-spacer au moins la hauteur du cadre, ou acceptez un premier bloc net d'emblée — dans une vraie page, c'est le sort des images au-dessus de la ligne de flottaison.
Quelle différence avec Scale In (fx-0536), View Timeline Scale (fx-0546) et Scale on Scroll (fx-0558) ?
La technique est la même dans les quatre : animation-timeline: view(), des images-clés qui mènent à scale(1) et à l'opacité 1, un remplissage both, aucun JavaScript, un défileur de 280 px. Les trois voisins animent des cartes de texte de 260 px (titre, paragraphe, fond blanc translucide) depuis scale(.7), sans montée. Scale In et Scale on Scroll utilisent animation-range: entry entry 50% : la carte atteint sa taille à mi-entrée, quand sa moitié basse est encore sous le cadre — acceptable pour un texte dont le titre est en haut, pas pour un visuel qu'on juge entier. View Timeline Scale utilise entry cover 50% : la carte continue de grossir jusqu'à ce que son centre atteigne celui du cadre, donc après qu'elle est entière. Scroll Fade Gallery borne la plage à entry 0% entry 100% sur des blocs 16/9 à 80 % de largeur, avec scale(.8) et 20 px de montée : taille finale et visibilité totale coïncident, et c'est le seul gratuit des quatre. Les quatre s'affichent à plat dans Firefox.
Comment l'utiliser dans une vraie page, avec des photos, sans le cadre de 280 px ?
Retirez .demo-preview et l'overflow du défileur : view() prend le conteneur de défilement le plus proche, à défaut la fenêtre, et rien d'autre ne change — gardez la règle .fade-gallery-item telle quelle. Mettez une <img> dans chaque bloc avec display: block; width: 100%; height: 100%; object-fit: cover et overflow: hidden sur le bloc pour garder les coins ; l'aspect-ratio: 16/9 réserve la hauteur avant le chargement, la timeline ne saute donc pas quand l'image arrive. Dans une colonne de 1 200 px sur une fenêtre de 900 px, une image à 80 % fait 960 × 540 : plus courte que la fenêtre, elle finit son entrée quand son bord bas touche le bas de l'écran, 540 px de molette ; sur un téléphone de 390 px, 292 × 164 px, 164 px de pouce. Les images déjà à l'écran au chargement sont nettes d'emblée.