Image Sequence
Une seule div dont 24 états (dégradé, arrondi, rotation, taille de 60 à 118 px) sont écrits dans un tableau JS et commutés d'après la position d'un conteneur à défilement de 280 px sur 920 px de course — un palier tous les 38 px, aucune image, 1,9 Ko de CSS et 3,5 Ko de JS. Les 12 premiers paliers sont presque invisibles sur le fond.
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
Le conteneur .scroll-container (100% × 280px, overflow: hidden auto, scroll-behavior: smooth, barre de 4 px stylée en ::-webkit-scrollbar seulement) contient .imgseq-inner, un bloc vide de 1200px : la course fait donc 1200 − 280 = 920 px. Dedans, .imgseq-display est en position: sticky; top: 0 avec une hauteur de 280px, celle du conteneur — la vitrine reste plein cadre du début à la fin. Elle porte le « film », .imgseq-frame, et le compteur .imgseq-counter (« Frame 1 / 24 », 10,4 px, blanc à 30 %). Le HTML vendu fixe déjà sur la frame, en attribut style, l'état 1 tel que le script l'avait laissé (dégradé #1a1a2e → #16213e, arrondi 20 px, 60 × 60) ; le script le réécrit au chargement, c'est inoffensif — mais les 120px du CSS ne s'appliquent jamais, pas plus que font-size: 1.5rem et font-weight: 800 sur une boîte sans texte. Le ::before est un anneau blanc à 10 % dont l'arrondi reste à 20 px quel que soit l'état, et .scroll-hint-label (« Scrollez pour explorer ») pulse de .3 à .7 d'opacité toutes les 2 s.
Le tableau frameStates contient 24 objets {bg, shape, rot, size}. À chaque événement scroll du conteneur, le script calcule r = scrollTop / (scrollHeight − clientHeight), puis i = min(floor(r × 24), 23), et écrit cinq styles en ligne : background, border-radius, transform: rotate(), width, height, plus le texte du compteur. Mesuré : les paliers tombent à 0, 39, 77, 115 … 882 px — 38,33 px chacun, le 24ᵉ tenant de 882 à 920. Le transition: .2s de la frame lisse la taille, l'arrondi et la rotation (60 → 118 px en 240 ms sur un saut de 0 à 700 px) mais pas le dégradé, qu'un navigateur n'interpole pas : il est déjà remplacé 61 ms après le saut. Avec une souris à crans (100 px par cran dans Chrome), un cran vaut 2,6 paliers : mesuré, la suite affichée est 1, 3, 6, 8, 11, 14, 16, 19, 21, 24 — 10 états sur 24 ; seuls un pavé tactile ou une roue libre passent par tous.
Les états eux-mêmes : douze dégradés sombres d'abord (du bleu nuit #1a1a2e → #16213e au prune #6a1a3c → #6e2168), dont le contraste avec le fond #0a0a0f mesure 1,20 à 1,83:1 — pendant les 460 premiers pixels, la moitié de la course, on voit un disque à peine plus clair que le noir grossir de 60 à 95 px et tourner de 0 à 165° ; à l'état 13 le dégradé saute à l'indigo #6366f1 → #7c3aed (3,84:1), puis violet, fuchsia, rose, orange, ambre, citron, cyan (12,24:1 à l'état 22) et retour à l'indigo. L'arrondi va de 20 à 10 px (états 1–7), passe au cercle (50%, 8–14), redescend de 45 à 25 % puis de 22 à 14 px ; la rotation avance de 15° par palier jusqu'à 345°. C'est un défilement imbriqué : mesuré avec 2000 px de page sous la zone, les dix premiers crans (920 px) sont absorbés par le conteneur avant que la page reprenne, et autant en remontant. L'écouteur scroll est posé sur le conteneur seul, jamais retiré — il disparaît avec l'élément ; pas de requestAnimationFrame, pas d'écouteur resize (la course est relue à chaque événement). Aucune console d'erreur, aucune variable CSS du site.
Accessibilité
- prefers-reduced-motion absent du code : mesuré sous
reduce, l'indication pulse toujours (pulseHint2 s infini), la transition de .2 s reste active et les états commutent — dont une rotation de 270° en 200 ms quand un saut de molette traverse 18 paliers. Le changement piloté par le défilement est initié par l'utilisateur (toléré par WCAG 2.3.3), pas la pulsation ni le tour sur lui-même : ajoutez@media (prefers-reduced-motion: reduce) { .imgseq-frame { transition: none } .scroll-hint-label { animation: none } }et mettezrotà 0 dans le tableau. - Clavier : la zone est un
divsanstabindex. Chromium rend pourtant les conteneurs défilants focalisables — mesuré : Tab arrive sur.scroll-container, chaque Flèche bas vaut 40 px, deux pressions affichent l'état 3 ; Firefox fait de même, Safari non. Poseztabindex="0"et unaria-label(ourole="region") pour que ce soit vrai partout ; Page suivante parcourt 245 px, soit six paliers d'un coup. - Lecteurs d'écran : le compteur est réécrit à chaque événement
scrollsansaria-live, la frame est undivvide et l'anneau un::before— rien n'est annoncé, rien n'est nommé. Mettezaria-hidden="true"sur.imgseq-frameet l'indication ; si l'état a un sens (nom d'une finition, numéro d'étape), annoncez-le dans une zonerole="status"mise à jour seulement quand l'index change — le script, lui, écrit ses cinq styles et le texte même quand l'index n'a pas bougé. - Contraste et textes : compteur et indication en
rgba(255,255,255,.3)sur#0a0a0fmesurent 2,62:1 en 10,4 px, sous les 4,5:1 — passez à.5(5,36:1) ou.6(7,30:1). Le compteur dit « Frame » (chaîne du JS), l'indication « Scrollez pour explorer » (HTML) : deux langues dans le code vendu. Enfin, la zone capte 920 px de molette avant de rendre la page — dix crans — sans autre signal que l'indication ; raccourcissez.imgseq-innerou gardez la zone à l'écart du fil de lecture principal.
Compatibilité navigateur
JavaScript ES2015 (const, fonction fléchée, gabarit de chaîne). CSS : position: sticky, inset, overflow à deux valeurs, font-variant-numeric, scroll-behavior. La commutation repose sur un écouteur scroll classique : elle fonctionne dans Firefox, contrairement aux effets de la catégorie écrits en animation-timeline: scroll() (fx-0585, fx-0545…), que Firefox n'active dans aucune version. Zéro dépendance.
Sans JavaScript, la zone défile ses 920 px pour rien : la frame reste à l'état 1 écrit en attribut (60 px, bleu nuit) et le compteur affiche « Frame 1 / 24 » (mesuré). Sans inset (navigateurs d'avant 2020), seul l'anneau blanc à 10 % disparaît. Sans position: sticky, la vitrine défile avec le bloc de 1200 px et sort du cadre.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
<div class="scroll-container" id="imgseq-scroll">
<div class="imgseq-inner">
<div class="imgseq-display">
<div class="imgseq-frame" id="imgseq-frame"></div>
<div class="imgseq-counter" id="imgseq-counter">Frame 1 / 24</div>
</div>
</div>
</div>
<span class="scroll-hint-label">Scrollez pour explorer</span>
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 |
|---|---|---|
Tableau des états (JS — frameStates) |
24 objets {bg, shape, rot, size} |
Ajoutez ou retirez des entrées : le compte suit (totalFrames = frameStates.length) et les paliers se recalculent (course ÷ n). bg accepte n'importe quelle valeur de background : liste de dégradés, couleur unie, url() d'une image préchargée. |
Course (CSS — .imgseq-inner { height: 1200px } et .scroll-container { height: 280px }) |
1200 − 280 = 920 px, soit 38 px par palier | La différence des deux hauteurs est la distance à parcourir. À 600 px de bloc, la course tombe à 320 px et un cran de molette (100 px) saute sept paliers ; visez au moins 100 px par état si une souris à crans doit tous les montrer. |
Rotation (JS — rot) |
0 → 345° par pas de 15° | Écrite en transform: rotate() et lissée par la transition : un saut de plusieurs paliers fait tourner la forme d'autant en 200 ms. Mettez 0 partout pour une photo de produit ou une figure. |
Taille (JS — size) |
60 → 118 (état 19) → 95 px | Appliquée à width et height : la frame est toujours carrée. Pour un rectangle, ajoutez un champ et écrivez les deux dimensions séparément. |
Transition (CSS — .imgseq-frame { transition: .2s }) |
.2s sur toutes les propriétés | Lisse taille, arrondi et rotation ; le dégradé, lui, saute toujours (mesuré : remplacé en 61 ms). 0s pour un film à crans francs, .4s pour un morphing plus lent, none sous prefers-reduced-motion. |
Palette (JS — bg) |
12 dégradés sombres (#1a1a2e → #6a1a3c) puis 12 vifs (#6366f1 → #22d3ee) |
La moitié sombre mesure 1,20 à 1,83:1 sur #0a0a0f, à peine visible. Commencez le tableau à l'état 13, ou éclaircissez les douze premiers ; sur fond clair, c'est l'inverse : les états citron et ambre faiblissent. |
Compteur (JS — `Frame ${i+1} / ${totalFrames}` ; CSS — .imgseq-counter) |
« Frame n / 24 », 10,4 px, blanc à 30 % | Réécrit à chaque événement. Remplacez le gabarit par un champ de l'état (t.name) pour afficher un nom ; montez l'opacité à .5 (5,36:1) pour passer le seuil de 4,5:1. |
Indication (HTML — .scroll-hint-label ; CSS — pulseHint) |
« Scrollez pour explorer », pulsation 2 s infinie | Texte en français codé dans le HTML, à traduire ou à retirer ; l'animation ne s'arrête jamais et n'est pas conditionnée à prefers-reduced-motion. |
FAQ
frameStates et posés sur une seule div ; rien n'est chargé. Pour de vraies photos, donnez à chaque état bg: "url(frame-01.jpg) center / cover", gardez size constant et rot: 0, et préchargez les fichiers (new Image().src = … au chargement) : l'échange se fait au moment du défilement, une image pas encore arrivée laisse la frame vide, et la transition de .2 s n'interpole pas plus deux images que deux dégradés. La version avec un vrai objet en 3D est fx-0795 (Pinned Image Sequence) : un modèle .glb dans <model-viewer>, donc WebGL et un fichier à héberger. Celle-ci coûte 5,4 Ko et tourne sur n'importe quel navigateur qui exécute du JavaScript.@keyframes à quatre arrêts (0, 25, 50, 75 %) sur animation-timeline: scroll(), sans JavaScript — mais aussi sans rien à lire : pas d'index, pas de nom, pas d'événement, et aucune version de Firefox n'active animation-timeline, où le bloc reste sur son premier dégradé. fx-0583 passe par un écouteur scroll : il fonctionne dans Firefox 66+, et il expose i et l'objet d'état à chaque événement — de quoi afficher un nom, synchroniser une légende ou déclencher un chargement. Le prix : six écritures de style par événement au lieu d'une animation composée. Prenez fx-0585 pour un décor que rien d'autre ne doit suivre, fx-0583 dès qu'un état a un nom ou que d'autres éléments doivent le suivre.#0a0a0f, sous les 3:1 attendus d'un élément graphique ; l'anneau ::before à 10 % de blanc fait 1,05:1 sur l'état 1. Pendant ces 460 premiers pixels — exactement la moitié des 920 px de course — seule la taille (60 → 95 px) et la rotation (0 → 165°) changent, sur une forme qu'on distingue à peine. À l'état 13 (dès 460 px), le dégradé saute à #6366f1 → #7c3aed (3,84:1) et la palette devient vive jusqu'à la fin. Deux remèdes : commencer le tableau à l'état 13 (douze paliers de 77 px, plus lisibles à la molette), ou remonter la luminosité des douze premiers dégradés.