Scroll Choreography Sequence
Sept éléments de texte qui entrent un à un — fondu, remontée de 22 px, zoom de 96 à 100 % — chacun dans sa fenêtre data-from / data-to d'une course de 2,2 hauteurs de scène, réversible à la molette. Ils n'en ressortent jamais ; sous prefers-reduced-motion, la scène s'affiche assemblée d'emblée, sans course. Zéro dépendance.
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
Anatomie : .scq-scene (width: 100%, aspect-ratio: 16/10, overflow: hidden, dégradé radial #20264f → #0a0e1f) contient un conteneur .scq-scroll en position: absolute; inset: 0; overflow-y: auto, barre masquée (scrollbar-width: none doublé de ::-webkit-scrollbar { width: 0 }). Dedans, un espaceur .scq-spacer de 320 % de la hauteur et un bloc .scq-sticky (position: sticky; top: 0, height: 31.25% — exactement 100 / 3,2, soit la hauteur visible du conteneur) qui centre la scène .scq-stage (480 px max, texte centré). Mesuré sur une page vierge de 900 px : scène de 884 × 552,5 px, contenu défilant de 1 768 px, course utile de 1 215 px (2,2 hauteurs) ; à 1 264 px de large, 790 px de haut et 1 738 px de course. Les sept .scq-el — kicker, titre, sous-titre, trois puces, bouton — portent chacun data-from et data-to : 0 → 0,2 ; 0,1 → 0,35 ; 0,25 → 0,5 ; 0,4 → 0,6 ; 0,5 → 0,7 ; 0,6 → 0,8 ; 0,8 → 1.
Le script (ES5, 1 804 caractères commentaire compris, une seule IIFE) parcourt chaque .scq-scene et pose un écouteur scroll passif sur le conteneur plus un resize sur window. À chaque événement, update() calcule la progression p = scrollTop / (scrollHeight − clientHeight), puis pour chaque élément t = clamp((p − from) / (to − from)) entre 0 et 1 — from et to lus par parseFloat(el.dataset.from) || 0 et || 1 — et l'écrit en variable CSS : el.style.setProperty('--t', t.toFixed(3)). Le CSS fait le reste, sans transition ni @keyframes : opacity: var(--t, 0) et transform: translateY(calc((1 − var(--t)) × 22px)) scale(calc(.96 + var(--t) × .04)), avec will-change: opacity, transform. Vérifié à p = 0,375, milieu de la fenêtre du sous-titre : --t: 0.501, opacité 0,501, translateY(10,98px), scale(.98). La barre .scq-prog i lit --scq-p (largeur calc(var(--scq-p) × 100%)) et le repère .scq-hint passe à opacity: 0 (transition .3 s) dès que p > 0.04 — 49 px de molette à cette taille.
C'est un scrubbing : rien n'est chronométré, la position fait tout, et le geste inverse démonte la scène (mesuré : retour en haut, les sept --t reviennent à 0). Les fenêtres se chevauchent — le titre commence à 0,1 quand le kicker est à moitié — d'où la cascade. Mais t ne redescend jamais une fois to dépassé : les éléments entrent et restent. À p = 1, les sept sont à --t: 1.000, réunis dans la même scène ; ce code ne fait pas sortir un élément pour laisser la place au suivant (la FAQ dit comment l'ajouter). Cas limite mesuré : data-from égal à data-to donne une division par zéro, ±Infinity que le clamp ramène à 0 puis 1 — une apparition instantanée à p = from. Ce qui est vendu se limite à cet effet : une IIFE de 1 804 caractères, sans variable globale, zéro bibliothèque, zéro image, zéro police à charger — le CSS déclare lui-même le fond (couleur pleine #0a0e1f sous le dégradé), la police (sans-serif) et le box-sizing de ses descendants, de sorte que la scène se comporte de la même façon collée dans une page vierge que sur cette page (mesuré : 0 requête, 0 erreur console, sept éléments à --t: 1.000 en bout de course). Les écouteurs scroll et resize restent en place tant que la page vit : si vous retirez une scène du DOM dynamiquement, retirez-les aussi, sinon le resize posé sur window garde la scène en mémoire. C'est l'architecture de Sticky Horizontal Reveal (fx-0796 : même conteneur défilant, même bloc épinglé, même barre, même repère au seuil 0,04) appliquée à des fenêtres par élément au lieu d'un déplacement horizontal.
Accessibilité
- prefers-reduced-motion : la scène s'affiche assemblée, sans course. Le script lit
matchMedia('(prefers-reduced-motion: reduce)')et, si la préférence est active, pose la classescq-reducedsur la scène avant de s'arrêter — c'est cette classe que le CSS attend : espaceur et bloc épinglé à100%(plus rien à faire défiler), sept éléments forcés àopacity: 1ettransform: nonepar!important, repère et barre endisplay: none. Mesuré avec la préférence active sur une page vierge de 1 280 px : course de 0 px, sept éléments à opacité 1 dès le chargement, contenu centré dans la scène, repère et barre absents. Le choix est délibéré : plutôt qu'un@mediaparallèle, une seule classe pilote l'état sans mouvement, et vous pouvez la poser à la main (voir personnalisation). - Clavier et repère : le conteneur
overflow-y: auton'a pas detabindexet sa barre est masquée sur tous les navigateurs. Mesuré dans Chromium : Tab atteint bien le conteneur (zones défilantes focalisables depuis Chrome 130) et deux flèches bas le font avancer de 80 px — mais ni Safari ni les Chrome antérieurs ne le font. Ajouteztabindex="0"et unaria-labelau.scq-scroll, et gardez une indication visible : le repère s'efface après 4 % de course, et la barre de 3 px (piste à 1,25:1 sur le fond, remplissage#5cffd8à 15,2:1) est ensuite le seul rappel qu'il reste à défiler. - Lecteurs d'écran : les sept éléments restent dans l'arbre d'accessibilité à toute position (opacité seule, ni
visibilitynidisplay), donc le texte complet est lu dans l'ordre du HTML quelle que soit la scène affichée — c'est le bon comportement, ne posez pasaria-hiddendessus. Le repère et la barre portent déjàaria-hidden="true". Le bouton « Commencer » est unspan: remplacez-le par unaou unbutton, et sachez qu'un élément à opacité 0 reçoit les clics (aucunpointer-events: none; mesuré àp = 0,elementFromPointau centre du bouton renvoie le bouton) — il est cliquable avant d'être visible. - Contraste et cadrage : mesuré dans Chromium sur les pixels sous le texte, kicker
#5cffd812,6:1, titre 15,9:1, sous-titre#c7cbe010,1:1, puces blanches sur 10 % de blanc 12,0:1, bouton#0a0e1fsur blanc 16,8:1, repère de 12 px 17,2:1 — tout passe. Le problème est ailleurs : la hauteur vient du seulaspect-ratio: 16/10. Mesuré à 360 px de large (scène de 344 × 215 px), le titre passe sur deux lignes et le contenu assemblé mesure 217 px : le bas du bouton dépasse de 22 px sous la barre de progression, coupé paroverflow: hidden(à 320 px : 217 px de contenu pour 200 de scène) ; à 364 px de scène, tout tient (187 px pour 227). Le repère, lui, se plie sur deux lignes dès que la scène fait moins de 396 px (left: 50%ne lui laisse que la moitié de la largeur ; mesuré : deux lignes à 384 px, une à 404 px). Donnez unemin-heightà la scène (ou un ratio plus haut sous 480 px) etwhite-space: nowrapau repère.
Compatibilité navigateur
JavaScript ES5 (var, function) avec dataset, NodeList.forEach, classList, matchMedia et style.setProperty sur une propriété personnalisée ; CSS : position: sticky, inset, variables dans calc() à l'intérieur de transform, will-change, isolation, et surtout aspect-ratio, seule source de la hauteur de la scène — sans lui, mesuré, la scène fait 0 px de haut. C'est lui qui fixe le plancher : Chrome 88, Firefox 89, Safari 15. Aucun animation-timeline : contrairement aux effets de défilement en CSS natif de cette catégorie (Stagger Cards fx-0540, Stagger Reveal CSS fx-0552, fx-0545, fx-0546…), que Firefox ne lit dans aucune version, cette chorégraphie fonctionne sur Firefox. scrollbar-width: none (Chrome 121, Firefox 64, Safari 18.2) est doublé par ::-webkit-scrollbar { width: 0 } : la barre est masquée partout. Zéro dépendance : aucune bibliothèque, aucune image, aucune police à charger — le fichier vendu ne contient que cet effet.
Sans JavaScript, la scène est vide : les sept éléments restent à opacity: 0 (valeur par défaut de var(--t, 0)), le repère « Scrollez pour composer la scène » s'affiche et 1 215 px de course défilent pour rien. Sans aspect-ratio (navigateurs antérieurs à 2021), la scène n'a aucune hauteur et n'occupe rien : donnez-lui une height explicite. Sous prefers-reduced-motion, le script pose scq-reduced : scène assemblée d'emblée, sans course (voir accessibilité).
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
<div class="scq-scene">
<div class="scq-hint" aria-hidden="true">Scrollez pour composer la scène</div>
<div class="scq-prog" aria-hidden="true"><i></i></div>
<div class="scq-scroll">
<div class="scq-spacer">
<div class="scq-sticky">
<div class="scq-stage">
<div class="scq-el scq-kicker" data-from="0" data-to="0.2">…</div>
<div class="scq-el scq-title" data-from="0.1" data-to="0.35">…</div>
<p class="scq-el scq-sub" data-from="0.25" data-to="0.5">…</p>
<div class="scq-chips">
<span class="scq-el scq-chip" data-from="0.4" data-to="0.6">…</span>
<span class="scq-el scq-chip" data-from="0.5" data-to="0.7">…</span>
<span class="scq-el scq-chip" data-from="0.6" data-to="0.8">…</span>
</div>
<span class="scq-el scq-cta" data-from="0.8" data-to="1">…</span>
</div>
</div>
</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 |
|---|---|---|
data-from / data-to (HTML, par élément) |
0 → 0,2 ; 0,1 → 0,35 ; 0,25 → 0,5 ; 0,4 → 0,6 ; 0,5 → 0,7 ; 0,6 → 0,8 ; 0,8 → 1 | Fenêtre d'entrée de chaque élément, en fraction de la course : --t vaut 0 avant from, 1 après to, linéaire entre les deux. Chevauchez les fenêtres pour une cascade, espacez-les pour un tour de rôle ; attribut absent = 0 et 1 (l'élément entre sur toute la course) ; from égal à to = apparition instantanée. Un data-to="0" est lu comme 1 (|| 1). |
Longueur de course (CSS — .scq-spacer { height: 320% } et .scq-sticky { height: 31.25% }) |
320 % / 31,25 % — 2,2 hauteurs de scène de molette (1 215 px à 884 px de large) | Les deux valeurs sont couplées : la hauteur du bloc épinglé doit rester 100 / (espaceur ÷ 100), sinon il dépasse ou laisse un vide. 250% ⇒ 40% : course de 1,5 hauteur, entrées plus rapides ; 400% ⇒ 25% : trois hauteurs, plus de temps par élément. |
Format de la scène (CSS — .scq-scene { width: 100%; aspect-ratio: 16/10 }) |
16/10, pleine largeur — 552 px de haut à 884 px, 790 px à 1 264 px, 215 px à 344 px | Seule source de la hauteur. Ajoutez max-width pour ne pas obtenir une scène de 790 px sur grand écran, et min-height: 360px (ou un ratio plus haut sous 480 px) pour que le contenu assemblé tienne sur mobile — mesuré, il déborde de 22 px à 360 px de large, dès que la scène fait 354 px ou moins. |
Amplitude d'entrée (CSS — .scq-el : 22px et .96 + … × .04) |
22 px de remontée, zoom de 96 à 100 % | Trajet de chaque élément entre --t 0 et 1, en plus du fondu. 40px pour une montée franche, 0px pour un fondu pur ; .9 + … × .1 accentue le zoom. Le tout reste sur le compositeur grâce à will-change. |
Seuil du repère (JS — p > 0.04) et son texte (HTML — .scq-hint) |
4 % de la course (49 px à 884 px de large), « Scrollez pour composer la scène » | Le repère s'efface (transition .3 s) dès ce seuil et revient si l'on remonte. Montez-le à 0.15 pour qu'il accompagne la première entrée ; ajoutez white-space: nowrap pour qu'il ne se plie pas sous 396 px de scène. |
| Couleurs (CSS) | fond #0a0e1f sous radial-gradient(circle at 50% 30%, #20264f, #0a0e1f 72%), accent #5cffd8 (kicker, barre), sous-titre #c7cbe0, puces à 10 % / 18 % de blanc, bouton blanc sur #0a0e1f |
L'accent sert deux fois (kicker et .scq-prog i) : changez-le aux deux endroits. La couleur pleine posée sous le dégradé est le fond effectif de la scène (repli si le dégradé ne s'applique pas, et ce que lisent les outils de contraste) : changez-la avec lui. Sur fond clair, inversez aussi .scq-stage { color }, le fond du repère (rgba(0,0,0,.4)) et la piste (rgba(255,255,255,.14)). |
Police (CSS — .scq-scene { font-family: sans-serif }) |
sans-serif (générique du système) |
La scène déclare sa police pour ne pas hériter de celle de la page hôte (Times par défaut sur une page vierge). Remplacez-la par celle de votre site, ou mettez inherit pour reprendre celle du contexte — c'est ce que font les trois scènes d'exemple ci-dessus. |
Classe scq-reduced (HTML, sur .scq-scene) |
absente (posée par le script sous prefers-reduced-motion) | État sans mouvement : espaceur et bloc épinglé à 100 % (plus de course), sept éléments forcés visibles par !important, repère et barre masqués, script inerte. Le script la pose lui-même quand prefers-reduced-motion est actif ; posez-la à la main pour livrer la scène assemblée sans la préférence système — capture, version imprimable, aperçu dans un e-mail. |
Nombre d'éléments (HTML — tout élément portant .scq-el) |
7 | Le script anime tout .scq-el de la scène, quel que soit son balisage ; le coût par événement scroll est de n écritures de --t. Chaque élément est une couche de composition (will-change) : au-delà d'une trentaine, retirez cette règle. |
FAQ
t = clamp((p − from) / (to − from)) ne fait que monter, et une fois to dépassé l'élément reste à --t: 1. Mesuré en bout de course, les sept éléments sont affichés ensemble — la « scène qui s'assemble » du catalogue est exacte, la scène qui se relaie n'existe pas. Pour l'obtenir, quatre lignes dans update() : lisez data-out et data-out-to (parseFloat(el.dataset.out), avec || 2 pour ne jamais sortir par défaut), calculez u = 1 − clamp((p − out) / (outTo − out)), et écrivez Math.min(t, u) à la place de t (vérifié : titre avec data-out="0.9" data-out-to="1", --t = 0,502 à p = 0,95 et 0 à p = 1). Comme --t pilote à la fois l'opacité et la remontée, la sortie rejoue l'entrée à l'envers : l'élément redescend de 22 px en s'effaçant. Donnez alors à chaque valeur du manifeste data-from="0" data-to="0.1" data-out="0.18" data-out-to="0.2", la suivante à partir de 0,2, et sortez-les du flux (position: absolute dans .scq-stage), sinon la place du précédent reste vide.overscroll-behavior absent) ne l'annonce. C'est le prix d'un conteneur imbriqué à barre masquée. Pour un vrai bloc de page : supprimez .scq-scroll, donnez à .scq-spacer une hauteur en vh (320vh) et à .scq-sticky height: 100vh ; dans le script, remplacez l'écouteur du conteneur par window.addEventListener('scroll', update) et calculez p = clamp(−rect.top / (spacer.offsetHeight − innerHeight)) avec rect = spacer.getBoundingClientRect(). Tout le reste — fenêtres, --t, barre, repère — est inchangé. Si vous gardez le conteneur, raccourcissez au moins l'espaceur à 250% / 40%.opacity et translateY(30px) sur sa propre timeline animation-timeline: view(), entre entry et entry 40% de son arrivée dans la fenêtre, avec un décalage fixe de 50 ms par :nth-child ; la cascade vient de la disposition — chaque carte entre quand elle apparaît — et se règle en CSS, sans attribut par élément. Scroll Choreography partage une horloge unique p pour tous les éléments d'une scène épinglée, et c'est le HTML qui dit qui entre quand : un titre à 10 %, un bouton à 80 %, sans rapport avec leur position dans la page. Deuxième différence, décisive selon votre audience : animation-timeline n'existe dans aucune version de Firefox, où Stagger Cards ne cascade pas ; fx-0797 y fonctionne. Prenez fx-0540 pour une grille de cartes qui se révèle au fil de la page sur Chrome et Safari ; fx-0797 pour une scène composée dans l'ordre que vous écrivez, partout.