Scroll✨ Premium

Scroll Velocity Feedback

Dix cartes dans une boîte qui défile : à chaque image, le script mesure le déplacement de scrollTop, le lisse à 20 % et l'écrit dans deux variables CSS — skewY jusqu'à 4° et scaleY jusqu'à +12 % à 30 px par image. Mesuré : une rafale plafonnée n'affiche que 1,5° à 120 Hz, la boîte de 420 px garde 284 px de course quelle que soit la largeur, et la boucle ne s'arrête jamais.

ScrollVelocityrequestAnimationFrame

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

Playlist d'une application DJ — feuilleter 1 248 titres avant le mix suivant — Scroll Velocity Feedback exemple 1

① Playlist d'une application DJ — feuilleter 1 248 titres avant le mix suivant

QuandBibliothèque de titres d'un logiciel de mix : un DJ cherche le prochain morceau à la molette ou au doigt, à toute vitesse, pendant que le titre en cours joue.
PourquoiAucun autre effet du lot ne réagit au rythme du geste : ils lisent tous scrollTop, une position. Ici les lignes se penchent d'autant plus que le DJ va vite — 2,1° à 16 px par image — et se redressent en une dizaine d'images dès qu'il s'arrête sur un titre : le feuilletage a une inertie, et la liste au repos est nette pour lire BPM et tonalité.
RéglagesGain et étirement du code vendu (4°, +12 %) ; transition: none sur les lignes pour que l'inclinaison suive la vitesse image par image ; grille à une colonne, lignes de 46 px ; le compteur devient une barre d'état « vitesse » sous la liste.
Fil de cotations d'une salle des marchés — le signal « trop vite » — Scroll Velocity Feedback exemple 2

② Fil de cotations d'une salle des marchés — le signal « trop vite »

QuandTableau de cours en direct : un opérateur survole 24 valeurs à la molette ; les lignes se penchent sous la vitesse et un badge apparaît quand la lecture n'est plus possible.
PourquoiLa déformation est une métadonnée comportementale — non pas « où tu es » mais « à quelle vitesse tu vas » — qu'aucun effet à position du lot ne peut exprimer. Le badge lit simplement --svf-a sur la première ligne : au-delà de 0,3 (9 px par image), les chiffres sont déclarés illisibles ; au repos, tout redevient droit et tabulaire.
RéglagesGain porté à 5° (skewY(calc(var(--svf-v, 0) * 5deg))), transition retirée ; badge .tr-alert allumé par une lecture de --svf-a > 0,3 dans une boucle rAF propre à la scène ; lignes de 38 px, chiffres en tabular-nums.
Bibliothèque de sprites d'un moteur de jeu — des vignettes qui se feuillettent — Scroll Velocity Feedback exemple 3

③ Bibliothèque de sprites d'un moteur de jeu — des vignettes qui se feuillettent

QuandOutil interne d'un studio : 56 vignettes d'assets (personnages, décors, effets) en grille de quatre colonnes, parcourues à la molette pour retrouver un fichier.
PourquoiCard Stack (fx-0587) révèle des cartes selon la position ; ici la grille prend une dimension physique proportionnelle à la dynamique de la molette : à 22 px par image, les vignettes s'inclinent de 5,9° et s'étirent de 9 %, comme des fiches qu'on feuillette, puis se réalignent au pixel. Sur des tuiles de 120 px, le gain vendu de 4° serait invisible.
RéglagesGain porté à 8° (* 8deg), étirement inchangé (+12 %), transition retirée ; grid-template-columns: repeat(4, 1fr), vignettes de 92 px ; compteur renommé « défilement ».

Comment ça marche

Anatomie : .svf-scene (height: 420px plafonnée par max-height: 100vh, overflow: hidden, isolation: isolate, fond #0c1022, texte blanc en sans-serif, arrondi 14 px) contient .svf-scroll, un conteneur absolu sur inset: 0 à overflow-y: auto, 20 px de marge interne et barre masquée (scrollbar-width: none doublé de ::-webkit-scrollbar { width: 0 }). Dedans, .svf-grid aligne dix .svf-card de 120 px sur deux colonnes (écart 16 px), quatre dégradés par nth-child(4n+1…4n), libellé « 01 » à « 10 » en blanc 15 px gras. Chaque carte porte transform: skewY(calc(var(--svf-v, 0) * 4deg)) scaleY(calc(1 + var(--svf-a, 0) * .12)), transition: transform .08s linear et will-change: transform. Le contenu fait 5 × 120 + 4 × 16 + 40 = 704 px pour une boîte de 420 px : 284 px de course quelle que soit la largeur (mesuré : scrollHeight − clientHeight = 284 à 1 280 px comme à 390 px). La hauteur est fixe, et non un ratio de la largeur, précisément pour cela : une boîte qui grandirait avec l'écran finirait par contenir tout son contenu et n'aurait plus rien à faire défiler. Le compteur .svf-meter (aria-hidden, 11 px, coin bas droit) affiche « vélocité » et .svf-val. Les dix cartes livrées portent en ligne style="--svf-v: 0.000; --svf-a: 0.000;" : un état de repos figé à l'extraction, sans effet puisque le script réécrit ces variables à chaque image.

La boucle : pour chaque .svf-scene, une fonction loop() en requestAnimationFrame lit scrollTop, calcule raw = cur − last (le déplacement depuis l'image précédente, en pixels) puis v += (raw − v) × 0.2 — un lissage exponentiel qui comble 20 % de l'écart par image. clamped = clamp(v, −30, 30) / 30 ramène la vitesse entre −1 et 1 ; la boucle refait scene.querySelectorAll('.svf-card') et écrit sur chacune --svf-v (signé, trois décimales) et --svf-a (valeur absolue), puis Math.round(|v|) dans le compteur, non plafonné. En descendant, --svf-v est positif : skewY fait plonger le bord droit de chaque carte ; en remontant, il remonte. Tout se compte en images, pas en millisecondes : après l'arrêt, v garde 33 % de sa valeur cinq images plus tard, 11 % après dix, moins de 0,5 % après vingt-quatre — 0,2 s à 120 Hz, 0,4 s à 60 Hz. Mesuré à 122 Hz : un cran de molette de 100 px appliqué en une image donne v = 20, --svf-a 0,667, compteur « 20 », retour à zéro en 180 ms ; un saut à bout de course (284 px en une image) donne v = 57 (compteur), plafonné à 1,000 pendant trois images.

Ce qui s'affiche est plus discret que la formule. La transition de 80 ms court après une cible qui perd 20 % par image : sur un geste bref, le transform calculé n'atteint jamais le nominal. Mesuré sur la première carte, rafale plafonnée (--svf-a = 1) : 1,47° et scaleY 1,044 à 122 Hz, 2,77° et 1,083 à 60 Hz émulé — 37 % puis 69 % des 4° / +12 % annoncés ; cran de 100 px : 0,74° / 1,022 à 122 Hz, 1,11° / 1,033 à 60 Hz ; retour visuel au net 350 ms après le geste. Seul un défilement soutenu approche la valeur nominale : 16 px par image maintenus donnent --svf-a 0,53, soit 2,1° — 1,31° mesurés après treize images. Deux conséquences : l'amplitude dépend de la fréquence de l'écran (plus forte à 60 Hz qu'à 120), et la boucle ne s'arrête jamais — mesuré au repos, sans aucun défilement : 2 400 setProperty et 120 querySelectorAll par seconde pour réécrire « 0.000 » sur dix cartes ; sous mouvement réduit, zéro écriture mais toujours 120 appels requestAnimationFrame par seconde.

Accessibilité

  • prefers-reduced-motion est présent et le repli est complet : matchMedia est lu une fois au chargement et, sous reduce, la boucle sort avant toute écriture — mesuré : 0 setProperty, transform identité après 300 px de molette, compteur figé sur « 0 », défilement natif intact (WCAG 2.3.3, animation déclenchée par l'interaction : respecté). Trois limites : un changement de préférence en cours de page n'est pas suivi (pas d'écouteur change) ; la classe svf-reduced que le CSS prévoit (transform: none !important) n'est jamais posée par le script, c'est un interrupteur manuel ; le compteur reste affiché à 0 — masquez-le sous @media (prefers-reduced-motion: reduce).
  • Barre de défilement masquée et boîte sans tabindex : scrollbar-width: none et ::-webkit-scrollbar { width: 0 } retirent la seule poignée visible, et rien n'indique que la zone défile. Mesuré dans Chromium : Tab atteint .svf-scroll (conteneurs défilants focalisables au clavier depuis Chrome 130), Flèche bas fait 40 px par appui (v de pointe 8, --svf-a 0,27) ; Firefox et Safari ne s'y arrêtent pas. Posez tabindex="0", role="region" et un aria-label sur .svf-scroll, avec un :focus-visible en outline-offset: -3px (l'overflow: hidden de la scène coupe un contour extérieur). Aucun overscroll-behavior : à bout de course, la molette passe à la page (mesuré : 284 px consommés par la boîte, le cran suivant fait défiler la page de 300 px).
  • Le texte se déforme avec la carte : skewY cisaille et scaleY étire tout ce que la carte contient, chiffres compris, pendant le geste. Réservez l'effet à des conteneurs (lignes, vignettes) et gardez-le hors des paragraphes qu'on lit en défilant. Contraste des libellés vendus (blanc 15 px gras, seuil 4,5:1) : de 1,25:1 sur l'extrémité menthe du premier dégradé à 3,1:1 au mieux — aucun ne passe ; ce sont des marqueurs de démonstration, remplacez-les par votre contenu ou passez le texte en #0c1022 (6,8:1 à 9,7:1 au milieu des quatre dégradés). Le compteur tient : 9,5:1 pour le libellé et 15,8:1 pour la valeur sur son fond noir à 45 %.
  • Lecteurs d'écran et tactile : le compteur est aria-hidden="true" — indispensable, il changerait jusqu'à 120 fois par seconde — et le script n'écrit que des propriétés personnalisées en ligne, l'arbre d'accessibilité n'est jamais touché. Les cartes sont des div sans rôle : dans une vraie liste, gardez vos ul/li ou vos liens et posez les classes dessus. Le libellé « vélocité » est codé en dur, en français, dans le HTML vendu. Au doigt, l'effet suit le glissé et l'inertie puisqu'il lit scrollTop et non des événements : mesuré en émulation tactile, glissé de 160 px en 130 ms → --svf-a 0,22, retour à zéro à la fin du geste.

Compatibilité navigateur

Code ES5 (var, function, IIFE), requestAnimationFrame, matchMedia, NodeList.forEach (Chrome 51, Firefox 50, Safari 10), variables CSS lues dans deux calc() d'un transform (Chrome 49, Firefox 31, Safari 9.1), will-change, isolation, inset et place-items. Le plancher réel est le raccourci inset (Chrome 87, Firefox 66, Safari 14.1), qui étire .svf-scroll sur toute la boîte ; la hauteur de .svf-scene est une simple height plafonnée par max-height: 100vh, comprise partout. scrollbar-width (Firefox 64, Chrome 121, Safari 18.2) est doublé de ::-webkit-scrollbar { width: 0 }. La vitesse est calculée par le script à partir de scrollTop, pas par animation-timeline : l'effet fonctionne dans Firefox, qui ne prend en charge les animations pilotées par le scroll en CSS natif dans aucune version — et aucune propriété CSS ne sait de toute façon exposer une vitesse, seulement une position. Zéro dépendance : aucune bibliothèque, aucune image, aucun fichier à héberger — les trois blocs collés dans une page vierge suffisent.

Chrome 87+✓ Complet
Firefox 66+✓ Complet
Safari 14.1+✓ Complet
Edge 87+✓ Complet
Mobile iOS 14.5+✓ Complet (mesuré : glissé de 160 px → --svf-a 0,22)
Android Chrome 87+✓ Complet

Sans JavaScript, la boîte défile normalement, les cartes restent droites (les variables gardent leur 0.000 en ligne) et le compteur affiche « 0 » — mesuré : transform identité après 300 px de molette. Sans le raccourci inset (navigateurs d'avant 2021), .svf-scroll n'est plus étiré sur la boîte : remplacez-le par top: 0; right: 0; bottom: 0; left: 0. Sans variables CSS, calc(var(--svf-v, 0) * 4deg) est invalide et toute la déclaration transform est ignorée : cartes droites, aucune erreur.

Le code

Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :

index.html — structure
<div class="svf-scene">
  <div class="svf-meter" aria-hidden="true">vélocité <b><span class="svf-val">0</span></b></div>
  <div class="svf-scroll">
    <div class="svf-grid">
      <div class="svf-card">01</div>
      <div class="svf-card">02</div>
      <div class="svf-card">03</div>
      <div class="svf-card">04</div>
      <div class="svf-card">05</div>
      <div class="svf-card">06</div>
      <div class="svf-card">07</div>
      <div class="svf-card">08</div>
      <div class="svf-card">09</div>
      <div class="svf-card">10</div>
    </div>
  </div>
</div>
🔒 Débloquer le code complet — à partir de 2,99 € le 1er mois

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éfautEffet
Angle maximal (CSS — skewY(calc(var(--svf-v, 0) * 4deg))) Inclinaison atteinte à 30 px par image ; le signe suit le sens du défilement. Sur un geste bref, le pic visible reste bien en dessous (1,5° mesuré à 120 Hz, 2,8° à 60 Hz) : 6 à 8° pour des tuiles étroites, 2° pour des lignes de texte.
Étirement maximal (CSS — scaleY(calc(1 + var(--svf-a, 0) * .12))) +12 % Allongement vertical, toujours positif (valeur absolue). Les cartes s'étirent depuis leur centre et mordent de 7 px sur leurs voisines à pleine vitesse ; 0 pour ne garder que l'inclinaison.
Plafond de vitesse (JS — Math.max(-30, Math.min(30, v)) / 30) 30 px par image Vitesse à laquelle l'effet sature. Un cran de molette de 100 px appliqué d'un coup donne 20 ; un glissé tactile mesuré, 7. 15 sature plus tôt et exagère les gestes doux ; 60 réserve la pleine amplitude aux grands coups.
Lissage (JS — v += (raw - v) * 0.2) 0,2 par image Fraction de l'écart comblée à chaque image : montée à 89 % en dix images, retour sous 0,5 % en vingt-quatre. 0.1 = inertie plus lourde (quarante-huit images), 0.5 = réaction sèche. Compté en images : deux fois plus long en millisecondes sur un écran 60 Hz.
Transition (CSS — transition: transform .08s linear) 80 ms Second lissage, en millisecondes celui-là, qui rabote le pic visible d'un geste bref (1,47° au lieu de 4° mesurés à 120 Hz). none pour que la carte suive la variable image par image — le choix des trois exemples ; .15s pour un rendu plus mou.
Contenu et hauteur de la boîte (HTML — 10 .svf-card de 120 px ; CSS — height: 420px; max-height: 100vh) 704 px de contenu, boîte de 420 px La course est la différence des deux : 284 px, quelle que soit la largeur. Mettez votre liste réelle (visez au moins deux hauteurs de boîte pour que la vitesse ait le temps de monter) ou changez la hauteur, en gardant la boîte plus basse que son contenu : à égalité, plus rien ne défile, donc plus rien ne bouge. Le plafond 100vh l'empêche seulement de dépasser l'écran.
Compteur (HTML — .svf-meter / .svf-val) « vélocité » + px par image, aria-hidden Affiche Math.round(|v|) sans plafond (57 mesuré pour un saut à bout de course, 284 px en une image). Supprimez le bloc entier : le script teste if (val). Le libellé est en français dans le HTML.
Sélection des cartes (JS — scene.querySelectorAll('.svf-card') dans la boucle) à chaque image Recherche les cartes 120 fois par seconde et écrit vingt variables. Sortez la requête de la boucle, ou écrivez les deux variables une seule fois sur .svf-grid — les cartes en héritent — pour diviser les écritures par dix.

FAQ

Parce que l'effet n'existe que s'il y a de la course, et que la course vaut scrollHeight − clientHeight : 704 px de cartes moins la hauteur de la boîte. Avec une hauteur proportionnelle à la largeur (aspect-ratio: 16/10, par exemple), la boîte grandit avec l'écran et finit par contenir tout son contenu — mesuré avec ce ratio : 214 px de course à 784 px de large, 89 à 1 000, zéro dès 1 127 px, et la molette passe directement à la page. Sans défilement, raw vaut toujours 0, v reste à 0, les cartes ne bougent pas et le compteur non plus. La hauteur fixe garantit 284 px de course partout (mesuré à 1 280 px comme à 390 px) ; max-height: 100vh l'empêche seulement de dépasser l'écran. Si vous changez la hauteur ou le contenu, vérifiez dans la console que s.scrollHeight − s.clientHeight reste franchement positif — visez au moins deux hauteurs de boîte pour que la vitesse ait le temps de monter.
La grandeur mesurée. Ces deux-là calculent une positionscrollTop / (scrollHeight − clientHeight), de 0 à 1 — dans un écouteur scroll passif, et la traduisent en remplissage ou en entrées d'éléments : arrêtez-vous à mi-course, la scène reste à mi-course. Celui-ci calcule une dérivée : la différence de scrollTop entre deux images, lissée, sans aucun écouteur ; arrêtez-vous n'importe où, tout revient à zéro en vingt-quatre images. C'est la seule information du lot qui décrit le geste et non l'endroit, et aucune animation pilotée par le scroll en CSS (animation-timeline) ne peut la produire : une timeline expose une position, jamais une vitesse. Le prix : une boucle requestAnimationFrame permanente là où les deux autres ne travaillent que pendant l'événement. Les trois s'empilent d'ailleurs sans conflit sur une même boîte — variables et classes différentes — si vous voulez une progression et un feedback de vitesse.
Oui, en permanence : loop() redemande une image avant même de tester quoi que ce soit. Mesuré au repos, boîte immobile : 120 appels requestAnimationFrame, 120 querySelectorAll et 2 400 setProperty par seconde pour réécrire 0.000 sur dix cartes — et encore 120 appels par seconde sous mouvement réduit, où la boucle ne fait rien. Sur une page qui reste ouverte, c'est un recalcul de style à chaque image pour rien. Correctif en six lignes : gardez un drapeau running ; dans loop(), si raw === 0 et |v| < 0.01, écrivez une dernière fois 0, mettez running = false et ne redemandez pas d'image ; sur scroller.addEventListener('scroll', …, { passive: true }), si !running, remettez last = scroller.scrollTop, running = true et relancez loop(). Sous mouvement réduit, ne lancez simplement pas la boucle : if (reduceMotion) return; avant l'appel. Pour une boîte hors écran, un IntersectionObserver peut couper de la même façon.