Scroll✨ Premium

Rotation on Scroll

Un cadran de 120 px à 12 graduations, collé à 50 px du haut d'une zone de 300 px qui défile sur 900 px : son aiguille (::before) tourne de 720° sur une timeline nommée — scroll-timeline: --rotationScroll y sur la zone, animation-timeline: --rotationScroll sur l'aiguille. Mesuré 0,8° par pixel, un tour tous les 450 px, retour au tick de départ en bas de course. Zéro JavaScript ; Firefox : aiguille immobile, cadran et textes visibles.

CSSscroll()Rotation

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

Molette de mouture d'une machine à café — tutoriel de réglage sur la fiche produit — Rotation on Scroll exemple 1

① Molette de mouture d'une machine à café — tutoriel de réglage sur la fiche produit

QuandFiche d'un moulin : le cadran gradué reste en haut, les consignes (espresso, filtre, piston) défilent dessous ; l'aiguille tourne au fur et à mesure vers le réglage décrit.
PourquoiLa position de l'aiguille sur les 12 graduations correspond à une étape du texte, et elle revient en arrière quand on remonte : c'est un réglage, pas une décoration — fx-0549 fait tourner un carré sans repère qui quitte l'écran.
RéglagesZone de 350 × 312 px à droite d'une colonne produit, fond clair ; bloc collant à top: 0, en ligne (cadran + légende), fond blanc et z-index: 2 pour que les fiches glissent dessous ; espaceur remplacé par quatre cartes (course mesurée 357 px) ; images-clés réduites à un tour (rotate(360deg)) pour qu'un cran vaille 30° ; aiguille et pivot en brun #8a5a2b, graduations sombres sur blanc. Le défilement automatique est un artifice de démonstration.
Manomètre d'une procédure de plongée — manuel en ligne d'un club — Rotation on Scroll exemple 2

② Manomètre d'une procédure de plongée — manuel en ligne d'un club

QuandPage « Paliers de décompression » : le manomètre reste épinglé, les paliers (3 m, 6 m, 9 m) défilent avec leurs durées ; l'aiguille tourne à mesure que la lecture avance.
PourquoiUn plongeur relit ses paliers dans les deux sens et le manomètre doit rester sous ses yeux jusqu'à la surface : le cadran est épinglé en haut de la zone sur toute la course, l'aiguille recule quand on remonte, les ticks majeurs marquent les quarts de tour — dans fx-0561, le carré monte avec la liste et sort du cadre avant le dernier palier, et ses anneaux pointillés ne portent aucune graduation.
RéglagesZone de 330 × 314 px sous une barre de 46 px, colonne de rappels à droite ; bloc collant en ligne (manomètre + titre) sur fond #082238 ; cinq paliers en liste (course mesurée 375 px, 720° conservés : 1,9° par pixel) ; aiguille et pivot ambre #ffb547, majeurs cyan #5cd6ff ; étiquette « Manomètre » à la place de « SCROLL TO ROTATE ».
Boussole d'un jeu d'aventure textuel — chapitre lu dans le navigateur — Rotation on Scroll exemple 3

③ Boussole d'un jeu d'aventure textuel — chapitre lu dans le navigateur

QuandPage d'un chapitre : la boussole reste en haut, le récit défile ; l'aiguille pivote deux tours complets pendant que le héros traverse la forêt, retour au nord à la dernière ligne.
Pourquoi720° sur toute la course = deux tours exacts, et la boussole reste épinglée sous la barre du chapitre du premier paragraphe au dernier : le lecteur voit l'aiguille repasser par le nord à mi-chapitre et s'y poser sur la dernière ligne — fx-0549 et fx-0561 tournent aussi de 720°, mais leur carré monte avec le texte et sort du cadre bien avant la fin du chapitre, et rien n'y marque le nord.
RéglagesZone pleine largeur (540 × 316 px) sous une barre de 44 px ; bloc collant en colonne comme dans le code vendu, top: 0, fond plein ; boussole = le cadran de 120 px entouré de N, E, S, O dans un carré de 142 px, aiguille rouge #e0563a, pivot et bord laiton ; sept paragraphes en Georgia et deux choix (course mesurée 327 px, 2,2° par pixel) : l'aiguille repasse par le nord à mi-chapitre et s'y arrête sur la dernière ligne.

Comment ça marche

Le HTML vendu empile un cadre .demo-preview (flex centré, min-height: 300px, position: relative, fond #0a0a0f, texte blanc, sans-serif), la zone .rotation-container (100% × 300 px, overflow: hidden auto, scroll-timeline: --rotationScroll y), un espaceur .rotation-spacer vide de 400% (1 200 px, padding-top: 20px) et le bloc .rotation-sticky (position: sticky; top: 50px, colonne centrée, gap: 12px) qui porte le cadran .rotation-dial — 120 × 120 px, bord de 3 px rgba(99,102,241,.3) — et l'étiquette .rotation-label « SCROLL TO ROTATE » (12 px, graisse 600, blanc à 50 %). Dans le cadran, douze .dial-tick de 2 × 8 px posés à top: 4px et tournés de 0 à 330° par pas de 30° (:nth-child(1) à (12)) autour de transform-origin: 50% 56px ; les 1er, 4e, 7e et 10e portent .dial-major (12 px, blanc à 60 %) et marquent les quarts de tour. L'aiguille est le pseudo-élément ::before : 3 px de large, 45 % de haut (51 px), dégradé transparent → #6366f1, ancré bottom: 50%; left: 50%, transform-origin: center bottom ; ::after pose le pivot, un disque de 10 px. Sous la zone, .slr-scroll-hint « Scrollez pour tourner » (11,2 px, blanc à 40 %, absolu à 8 px du bas du cadre) rebondit de 4 px toutes les 2 s. Course mesurée : 1 200 − 300 = 900 px. Aucun JavaScript, aucune variable du site, aucune erreur console.

Le mouvement tient en deux déclarations. scroll-timeline: --rotationScroll y sur la zone crée une timeline nommée qui vaut 0 en haut et 1 en bas de sa course ; animation-timeline: --rotationScroll sur ::before y attache l'animation dialRotate (translateX(-50%) rotate(0deg) → rotate(720deg), linear, animation-range: normal = toute la course, fill-mode: both, animation-duration: auto). Le navigateur remplace l'horloge par la position de défilement : l'angle vaut 720° × scrollTop ÷ 900, sans écouteur ni calcul. Mesuré dans Chromium par crans de molette de 150 px : 120°, 240°, 360°, 480°, 600°, 720° — 0,8° par pixel, un tour tous les 450 px, une graduation tous les 37,5 px — progress relu à 0,1667 par cran ; mêmes valeurs dans WebKit 26.5. Flèche bas (40 px) donne 32°, PageDown (302 px) 241,6°, Fin 720°. Remonter fait tourner l'aiguille à l'envers, sans inertie ni lissage. Le nom de la timeline se résout en remontant les ancêtres du pseudo-élément : deux copies du code collées dans la même page suivent chacune leur zone (mesuré : 0° et 180° avec la seconde à 225 px), et une zone peut piloter plusieurs cadrans.

Le cadran, lui, ne se déplace pas : top: 50px pousse le bloc collant à 50 px du haut de la zone dès le repos (sa place naturelle, 20 px, est sous le seuil) et l'y maintient sur les 900 px — mesuré : bloc à 50 px à toutes les positions, 146 px de haut, aiguille et étiquette toujours dans le cadre. C'est ce qui le sépare de Rotate on Scroll (fx-0561) et Rotate On Scroll (fx-0549), où l'objet qui tourne est dans le flux et traverse la boîte. La zone était vendue en height: 100% : sous un parent sans hauteur fixe — une page vierge, l'aperçu du site — elle prenait la hauteur de son contenu (166 px), l'espaceur en pour cent s'écrasait et il n'y avait rien à faire défiler (course mesurée 0, aiguille figée). Elle fait maintenant 300 px en dur ; changez cette valeur, l'espaceur suit.

Accessibilité

  • prefers-reduced-motion absent du code : mesuré sous émulation, l'aiguille suit toujours le défilement (progression 0,5 à 450 px) et l'indice continue de rebondir (animation mesurée running). La rotation est acceptable telle quelle : elle ne bouge que par le geste du lecteur (WCAG 2.3.3 vise le mouvement autonome). Le rebond, lui, est continu : ajoutez @media (prefers-reduced-motion: reduce) { .slr-scroll-hint { animation: none } }.
  • Clavier : mesuré, Chromium et Firefox donnent le focus à la zone par Tab sans tabindex (activeElement = .rotation-container) ; flèche bas avance de 40 px (32°) dans Chromium, 51 px dans Firefox ; PageDown de 302 px (241,6°), Fin va au bout. WebKit 26.5 ne la focalise pas (focus resté sur body, flèches sans effet) : ajoutez tabindex="0", un aria-label et un style :focus-visible. Au doigt, le geste doit commencer dans la zone ; overscroll-behavior: contain évite d'entraîner la page en bout de course.
  • Contraste sur #0a0a0f : l'étiquette (blanc à 50 %, 12 px) vaut 5,3:1 ; l'indice (blanc à 40 %, 11,2 px) 3,8:1, sous le seuil de 4,5:1 — passez-le à 60 % ou posez aria-hidden="true", il est décoratif. Les ticks à 30 % valent 2,6:1, sous le 3:1 des éléments graphiques (les majeurs à 60 %, 7,3:1) ; l'aiguille et le pivot #6366f1 4,4:1 ; le bord du cadran 1,4:1, décoratif. Si la position de l'aiguille doit être lue, montez les ticks à 50 %.
  • Lecteurs d'écran et repli : le cadran est fait de div vides, rien n'est lu et l'angle n'est pas exposé — si l'aiguille porte une information (un réglage, une position dans le texte), écrivez-la en texte à côté, l'effet n'a pas de script pour le faire, et masquez le dessin avec aria-hidden="true". « SCROLL TO ROTATE » et « Scrollez pour tourner » sont deux textes en dur, un en anglais, un en français, lus tels quels quelle que soit la langue de la page. Sans animation pilotée par le défilement (Firefox), le cadran est un dessin fixe et la zone défile ; sans CSS, une pile de div vides et deux lignes de texte.

Compatibilité navigateur

CSS seul : scroll-timeline (timeline nommée), animation-timeline, animation-range, position: sticky, overflow: hidden auto, un dégradé et deux @keyframes. Zéro dépendance, zéro JavaScript. Les animations pilotées par le défilement existent dans Chrome et Edge 115+, Safari et iOS 26+ (mesuré dans WebKit 26.5 : 120° par cran de 150 px, valeurs identiques à Chromium). Firefox : aucune version. Mesuré dans Firefox 153 : CSS.supports('animation-timeline: scroll()') répond faux, la déclaration et animation-duration: auto sont ignorées (durée calculée 0s), dialRotate tourne sur l'horloge du document et se termine aussitôt (playState: finished, progression 1, fill-mode: both) : l'aiguille prend son image-clé finale, rotate(720deg) — deux tours exacts, donc visuellement identique au repos — et n'en bouge plus. La zone défile normalement (course 900 px), le cadran reste collé à 50 px, graduations, étiquette et indice sont affichés ; aucune erreur. Un visiteur Firefox voit un cadran fixe dont l'aiguille pointe le tick du haut, sous une invitation à défiler qu'il ne peut pas vérifier.

Chrome 115+✓ Complet
Firefox✓ Aucune version : aiguille figée au tick du haut, cadran et textes visibles
Safari 26+✓ Complet (mesuré dans WebKit 26.5)
Edge 115+✓ Complet
Mobile iOS 26+✓ Complet (défilement tactile de la zone)
Android Chrome 115+✓ Complet

Le code vendu n'a pas de repli @supports et n'en a pas besoin pour rester lisible : l'image-clé finale est l'état de départ, rien n'est caché ni déplacé. Dans Firefox, Chrome avant 115 et Safari avant 26, le cadran est un dessin fixe et la zone une boîte qui défile. Si l'aiguille porte une information, donnez-la autrement à ces navigateurs, ou retirez l'invitation qui promet une rotation : @supports not (animation-timeline: scroll()) { .rotation-label, .slr-scroll-hint { display: none } }. Pour une rotation réelle partout, il faut un écouteur scroll qui écrit le transform, comme Zoom on Scroll (fx-0580) le fait pour son échelle. Là où overflow n'est pas honoré (certains clients mail), l'espaceur s'étire à 1 200 px et le cadran reste posé en haut, immobile.

Le code

Structure HTML à coller dans votre page (le CSS + JS sont inclus avec Premium) :

index.html — structure
<div class="demo-preview">
  <div class="rotation-container">
    <div class="rotation-spacer">
      <div class="rotation-sticky">
        <div class="rotation-dial">
          <div class="dial-ticks">
            <div class="dial-tick dial-major"></div><div class="dial-tick"></div><div class="dial-tick"></div>
            <div class="dial-tick dial-major"></div><div class="dial-tick"></div><div class="dial-tick"></div>
            <div class="dial-tick dial-major"></div><div class="dial-tick"></div><div class="dial-tick"></div>
            <div class="dial-tick dial-major"></div><div class="dial-tick"></div><div class="dial-tick"></div>
          </div>
        </div>
        <span class="rotation-label">…</span>
      </div>
    </div>
  </div>
  <span class="slr-scroll-hint">…</span>
</div>
Débloquer le code complet — 9,90 € HT/mois

Le code complet HTML + CSS + JS de Rotation on Scroll, 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éfautEffet
Angle total (CSS — @keyframes dialRotate, to { rotate(720deg) }) 720° (deux tours, 0,8° par pixel sur 900 px) L'aiguille retombe sur le tick de départ en bas de course. 360deg pour un tour par lecture, 330deg pour aller du premier au douzième tick sans revenir, 270deg pour une jauge à quart mort. Gardez le translateX(-50%) dans les deux images-clés : il centre l'aiguille sur le pivot.
Hauteur de la zone et course (CSS — .rotation-container { height: 300px }, .rotation-spacer { height: 400% }) 300 px / 1 200 px, course 900 px Course = espaceur − zone ; angle par pixel = angle total ÷ course. 200% donnent 300 px de course et 2,4° par pixel ; 800%, 2 100 px et 0,34°. Une hauteur en pour cent sur la zone ne marche que sous un parent de hauteur fixe — c'est ce qui la rendait inerte collée seule.
Point d'accroche (CSS — .rotation-sticky { top: 50px }, .rotation-spacer { padding-top: 20px }) 50 px (bloc collant de 146 px) Le bloc est poussé à 50 px dès le repos parce que sa place naturelle (20 px) est sous le seuil ; top: 20px le laisse en place et ne le colle qu'ensuite. Le bloc doit tenir dans la zone : 50 + 146 = 196 sur 300.
Graduations et taille du cadran (HTML + CSS — douze .dial-tick, :nth-child(n) de 30° en 30°, transform-origin: 50% 56px, .rotation-dial 120 px) 12 ticks, 4 majeurs à 0°, 90°, 180°, 270°, cadran de 120 px Pour 8 ticks, huit div et des :nth-child tous les 45°. L'origine à 56 px vise un point 3 px sous le centre (4 + 56 = 60, centre de la boîte intérieure à 57 ; mesuré : tick du haut à 47 px du centre, tick du bas à 53) — 50% 53px corrige. L'aiguille suit la taille du cadran en pour cent, le pivot et les ticks non : pour 160 px, origine 50% 73px et bloc collant de 186 px, donc top ≤ 114 px.
Couleurs (CSS — #6366f1 dans le dégradé de ::before, ::after et le bord ; blanc à .3 et .6 des ticks) indigo, ticks à 30 et 60 % L'aiguille est un dégradé transparent → #6366f1 : sa base s'efface sous le pivot, sa pointe est pleine. Ticks à .3 = 2,6:1 sur #0a0a0f, .5 passe le 3:1. Sur fond clair, changez aussi le fond du cadre .demo-preview et le blanc des textes.
Cible du défilement (CSS — scroll-timeline: --rotationScroll y) la zone de 300 px Portez la déclaration sur html pour lier l'aiguille au défilement de la page (mesuré sur une page de 3 000 px : 180° à 600 px, 720° en bas), ou remplacez animation-timeline par scroll(root). Le cadran doit rester un descendant de l'élément qui porte le nom, en sticky ou fixed pour rester visible.
Étiquette et indice (HTML — .rotation-label « SCROLL TO ROTATE », .slr-scroll-hint « Scrollez pour tourner ») anglais et français en dur, rebond de 4 px toutes les 2 s Deux textes en dur dans deux langues : remplacez-les ou supprimez les span. L'indice s'accroche au cadre .demo-preview (position: relative) : sans ce cadre, il se cale sur le premier écran de la page, plus sur la zone ; hintBounce part avec lui.

FAQ

Remplacez le contenu de .rotation-spacer par votre texte en gardant .rotation-sticky en premier : il reste collé pendant que le texte passe dessous. Donnez-lui un fond et un z-index, sinon les paragraphes suivants se peignent par-dessus le cadran ; top: 0 si le cadran doit affleurer. La course devient hauteur du contenu − 300 px et les 720° s'y répartissent d'eux-mêmes : 1 500 px de texte donnent 1 200 px de course, 0,6° par pixel. Deux limites : le cadran doit rester à l'intérieur de .rotation-container, car --rotationScroll se résout en remontant ses ancêtres — un cadran posé à côté de la zone ne tourne pas (timeline-scope sur un ancêtre commun lève cette limite, Chrome 116+ et Safari 26) ; et la zone garde une hauteur fixe, à remplacer par une valeur en vh ou par un parent qui en a une. Les trois scènes de cette page sont construites ainsi (courses mesurées 357, 375 et 327 px).
Même angle, même famille CSS, trois objets différents. fx-0549 fait tourner un carré de 100 px à dégradé indigo-fuchsia avec une icône SVG, posé au milieu d'un plateau de 800 px dans une boîte de 280 px, sur animation-timeline: scroll() — la timeline anonyme du défileur le plus proche. fx-0561 fait pareil avec un carré de 120 px violet-cyan et deux anneaux en pointillés animés eux aussi (−360° pour l'un, +720° pour l'autre). Dans les deux, l'objet est dans le flux : il monte avec le contenu, traverse la boîte et sort par le haut, et rien ne dit où l'on en est. fx-0571 déclare une timeline nommée (scroll-timeline: --rotationScroll y) et colle un instrument : le cadran reste à 50 px du haut pendant toute la course, ses douze graduations donnent une lecture (un tick tous les 37,5 px), et seul le pseudo-élément tourne. Le nom permet aussi de choisir le défileur — une zone parente précise, ou la page via html — là où scroll() prend toujours le plus proche. Prenez fx-0549 ou fx-0561 pour un objet décoratif qui passe, fx-0571 quand la rotation doit lire une position (un réglage, une progression, un cap) et rester sous les yeux.
Parce que 720° font deux tours entiers : à mi-course (450 px) l'aiguille est déjà revenue au tick de départ, et en bas elle y est de nouveau — mesuré 0° à 0, 450 et 900 px, 180° à 225 px. Le cadran seul ne distingue donc pas le quart des trois quarts ; c'est voulu pour un mouvement qui se referme (une boussole qui retrouve le nord, une molette qui fait deux tours), pas pour une jauge. Pour une lecture univoque, ramenez l'image-clé finale à rotate(360deg) (un tick = 1/12 de la course) ou rotate(330deg) (l'aiguille va du premier au douzième tick et s'y arrête). La position se lit ensuite sur les graduations : avec 720° sur 900 px, un tick vaut 37,5 px et un quart de tour 112,5 px ; une flèche bas de 40 px déplace l'aiguille de 32°, un PageDown de 302 px de 241,6° (mesuré). Rien n'est exposé aux lecteurs d'écran : si la position compte, écrivez-la aussi en texte.