Parallax Layers
Un cadre de 280 px qui défile sur 620 px, un seul écouteur scroll et six écritures de style par événement : halo de fond remonté de 60 px et grossi ×1,5, trois formes tournées de 180°, mot grossi ×1,8 et fondu à 50 % — sauf que les deux couches avant sont en position: absolute et quittent le cadre avant 26 % de la course.
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 : .parallax-viewport#parallax-scroll est le conteneur défilant — width: 100%, height: 280px, overflow: hidden auto, barre WebKit de 4 px stylée. Il contient .parallax-scroll-space, un bloc de 900 px en position: relative : la course est de 900 − 280 = 620 px. Trois couches y vivent. #plx-bg (.st-parallax-layer : position: sticky; top: 0, 280 px, flex centré, pointer-events: none, z-index: 1) porte un .plx-element de 200 px, disque radial-gradient(rgba(99,102,241,.15), transparent 70%) sous filter: blur(20px) — mesuré en fin de course, son point le plus clair vaut rgb(20,21,40) sur #0a0a0f, à peine une lueur. #plx-mid (position: absolute; top: 0, 280 px, z-index: 2) aligne dans un .plx-element flex à gap: 12px trois .plx-shape de 40 px : un losange (bordure 2 px violet à 40 %, rayon 8 px, rotate(45deg)), un cercle (bordure rose à 40 %) et un triangle (clip-path: polygon(50% 0, 0 100%, 100% 100%), cyan à 20 %). #plx-fg (absolu aussi, z-index: 3) centre un span.plx-text « PARALLAXE » en 1,4 rem gras 800 — 135 × 25 px mesurés —, dégradé #a855f7 → #ec4899 découpé par background: … text et -webkit-text-fill-color: transparent. Sous le tout, .scroll-hint-label « Scrollez pour explorer » : absolu, 8 px du bas, 0,65 rem, blanc à 30 %, opacité qui pulse de .3 à .7 toutes les 2 s (pulseHint).
Le JavaScript tient en un écouteur scroll sur le conteneur, sans requestAnimationFrame, sans lissage. À chaque événement il lit l = scrollTop et e = l / (scrollHeight − clientHeight) — la progression de 0 à 1 sur les 620 px — puis écrit six styles en ligne, tous linéaires : le disque de fond reçoit translateY(−60·e px) scale(1 + .5·e) ; la couche médiane translateY(−.3·l px) et ses trois formes rotate(180·e deg) ; la couche avant translateY(−.6·l px), le mot scale(1 + .8·e) et opacity: 1 − .5·e. Mesuré à la molette dans Chromium, à 300 px (e = 0,484) : fond −29 px / ×1,24, médiane −90 px, rotation 87°, avant −180 px, mot ×1,39 à 76 % d'opacité ; en butée : −60 px / ×1,5, −186 px, 180°, −372 px, ×1,8 à 50 %. Aucune inertie : chaque valeur est exactement proportionnelle au défilement et revient sur ses pas quand on remonte. Chaque événement coûte quatre querySelector et six écritures de style ; l'écouteur n'est jamais retiré.
La géométrie contredit la promesse des « trois vitesses ». Seule la couche de fond est sticky ; la médiane et l'avant-plan sont position: absolute; top: 0 dans l'espace qui défile : elles montent déjà à la vitesse du contenu, et la translation négative s'y ajoute — 1,3× et 1,6× le défilement, vers le haut. Mesuré : le mot sort du cadre à 100 px de course (e = 0,16, à peine ×1,13 et 92 % d'opacité), les trois formes à 160 px (e = 0,26, 46° de rotation). Sur les 620 px, 520 ne montrent plus que la lueur collante qui glisse de 60 px. Les 180°, le ×1,8 et le fondu à 50 % s'exécutent hors champ. Deux indices d'un code remanié : .parallax-viewport est déclaré deux fois, d'abord position: sticky; top: 0 puis position: relative — la première règle est morte —, et la classe .st-parallax-layer (le collant) n'est posée que sur le fond. Pour une parallaxe à trois couches sur toute la course, il faut les rendre toutes collantes et superposées, et réduire les deux translations (cf. FAQ) ; le reste du script ne change pas.
Accessibilité
- prefers-reduced-motion absent du code. Mesuré sous
reduce:pulseHinttourne toujours et, à 300 px de défilement, la médiane est tournée de 87° et le mot grossi ×1,39. Une parallaxe est le cas typique de WCAG 2.3.3 (animation déclenchée par l'interaction). Conditionnez l'ajout de l'écouteur à!matchMedia('(prefers-reduced-motion: reduce)').matches— les couches restent alors à leur position de repos, un repli honnête — et ajoutez@media (prefers-reduced-motion: reduce) { .scroll-hint-label { animation: none } }. - Le repère « Scrollez pour explorer » est codé en français, en 10,4 px, blanc à 30 % sous une opacité qui pulse : contraste calculé de 1,22:1 à 1,83:1 sur
#0a0a0f(2,62:1 sans la pulsation), loin des 4,5:1. Il est aussi positionné par rapport au wrapper.demo-preview: collé sans lui, mesuré dans une fenêtre de 600 px, il tombe à y = 579 alors que le conteneur finit à y = 400. Soit vous en faites une consigne lisible (14 px, blanc à 70 %), soit vous le passez enaria-hidden="true"et mettez la consigne dans l'aria-labeldu conteneur. - Clavier et lecteurs d'écran. Le conteneur
overflow: auton'a aucun enfant focalisable : Chrome 130+ et Firefox le rendent focalisable au Tab (les flèches défilent, l'effet suit), Safari non — poseztabindex="0",role="region"et unaria-label. Le mot « PARALLAXE » est un vrai texte, lu par les lecteurs d'écran ; les formes sont desdivvides, muettes. Si les couches sont décoratives,aria-hidden="true"sur.parallax-scroll-spaceévite d'annoncer un mot isolé dans une zone qui défile pour rien. - Contraste des couches. Le mot :
#a855f74,99:1 et#ec48995,6:1 sur#0a0a0fau repos (22 px gras : seuil 3:1 en grand texte, tenu), mais 2,05:1 à 2,15:1 à 50 % d'opacité en fin de course, et 3,96:1 / 3,53:1 sur fond blanc. Les formes, décoratives (seuil 3:1), sont sous le seuil : bordures à 40 % = 1,70:1 et 1,76:1, triangle à 20 % = 1,45:1 ; montez les alphas à .8 si elles doivent se voir. Le halo de fond est à 1,14:1 : invisible sur un fond clair.
Compatibilité navigateur
Requiert ES2015 (const, fonctions fléchées, gabarits de chaîne), position: sticky, clip-path: polygon(), filter: blur(), la syntaxe à deux valeurs overflow: hidden auto et le mot-clé text dans le raccourci background — c'est lui qui fixe le seuil Chrome. Aucune bibliothèque, aucune variable CSS, aucune animation-timeline : la parallaxe est calculée en JavaScript sur l'événement scroll, elle fonctionne donc dans Firefox, contrairement aux cinq autres parallaxes de la catégorie (fx-0538, fx-0548, fx-0557, fx-0573, fx-0592), pilotées au défilement en CSS natif — mesuré dans Firefox 153 : CSS.supports('animation-timeline: scroll()') répond faux et fx-0548 n'y bouge pas, quand fx-0576 y tourne de 73° après une molette de 300 unités. Mêmes transformations, même dégradé de texte et même triangle mesurés dans WebKit.
Sans JavaScript, la zone défile mais rien ne se transforme : la lueur reste collée, les formes et le mot montent avec le contenu et sortent du cadre. Chrome et Edge antérieurs à 120 (décembre 2023) rejettent text dans le raccourci background : la déclaration entière tombe et, avec -webkit-text-fill-color: transparent, le mot est invisible — ajoutez -webkit-background-clip: text à côté. Sans clip-path (Safari avant 13.1 sans préfixe), le triangle est un carré cyan.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
<div class="parallax-viewport" id="parallax-scroll">
<div class="parallax-scroll-space">
<div class="st-parallax-layer parallax-bg-layer" id="plx-bg">
<div class="plx-element"></div>
</div>
<div class="parallax-mid-layer" id="plx-mid">
<div class="plx-element">
<div class="plx-shape"></div>
<div class="plx-shape"></div>
<div class="plx-shape"></div>
</div>
</div>
<div class="parallax-fg-layer" id="plx-fg">
<span class="plx-text">PARALLAXE</span>
</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 |
|---|---|---|
Course (CSS — .parallax-scroll-space height 900px) |
900 px, soit 620 px de course pour un cadre de 280 px | e est normalisé : les amplitudes (−60 px, 180°, ×1,8) sont atteintes en butée quelle que soit la hauteur ; allonger la course ralentit tout. Mais −.3·l et −.6·l sont en pixels absolus : à 1 500 px de course, la médiane monte de 450 px. |
Hauteur du cadre (CSS — .parallax-viewport, .st-parallax-layer, .parallax-mid-layer, .parallax-fg-layer : 280px) |
280 px, quatre fois | Les quatre valeurs changent ensemble : les couches sont dimensionnées à la main, pas en height: 100%. Le wrapper .demo-preview impose en plus min-height: 300px. |
Fond (JS — translateY(${-60*e}px) scale(${1+.5*e})) |
−60 px et ×1,5 en butée | Amplitude du glissement et du zoom du disque de fond. −120 / 1 pour un fond qui monte sans grossir. Le disque de 200 px flouté à 20 px reste quasi invisible (1,14:1) : montez l'alpha du dégradé de .15 à .4 pour qu'on le voie. |
Couche médiane (JS — translateY(${-.3*l}px) et rotate(${180*e}deg)) |
−0,3 px par pixel défilé, 180° en butée | La translation s'ajoute au défilement naturel de la couche (absolue) : 1,3× au total. 360 pour un tour complet ; 0 pour une couche qui ne fait que glisser. Les trois formes tournent comme un bloc autour du centre de .plx-element. |
Avant-plan (JS — translateY(${-.6*l}px), scale(${1+.8*e}), opacity = 1 − .5*e) |
−0,6 px par pixel, ×1,8 et 50 % en butée | 1 − e pour un mot qui disparaît complètement ; 1 + 2·e pour un zoom de titre. C'est le span entier qui grossit, pas la police : le texte reste net. |
Superposition des couches (CSS — .parallax-mid-layer, .parallax-fg-layer : position absolute) |
absolues dans l'espace défilant (sortie du cadre à 100 et 160 px) | Pour une parallaxe sur toute la course : .parallax-scroll-space { display: grid } et, sur les trois couches, grid-area: 1 / 1; position: sticky; top: 0 ; puis −.15·l et −.2·l dans le script. Mesuré ainsi sur 280 px et 620 px de course : formes et mot visibles jusqu'en butée (le mot à 86 %). Avec le collant seul, le mot sort encore à 310 px. |
| Couleurs (CSS — halo rgba(99,102,241,.15), bordures .4, dégradé #a855f7 → #ec4899) | indigo / violet, rose, cyan / violet → rose | Le dégradé du mot est découpé par background: … text : ajoutez -webkit-background-clip: text pour Chrome et Edge antérieurs à 120, sinon le mot y est transparent. Sur fond clair, le mot tient 3,96:1 / 3,53:1 : assombrissez vers #7e22ce → #be185d. |
Repère (HTML — .scroll-hint-label) |
« Scrollez pour explorer », 10,4 px, blanc à 30 %, pulse 2 s | Texte à traduire ou à retirer ; il dépend du position: relative de .demo-preview. Sous prefers-reduced-motion, coupez pulseHint. |
FAQ
#plx-bg porte la classe .st-parallax-layer (position: sticky) ; #plx-mid et #plx-fg sont position: absolute; top: 0 à l'intérieur de .parallax-scroll-space, qui défile. Elles montent donc à la vitesse du contenu, plus leur translation (−.3·l, −.6·l) : 1,3× et 1,6× le défilement. Les 180° de rotation, le ×1,8 et le fondu se jouent hors champ. Correctif en trois règles et deux nombres : .parallax-scroll-space { display: grid }, sur les trois couches grid-area: 1 / 1; position: sticky; top: 0 — elles se superposent dans la même cellule et restent collées pendant toute la course, seules les translations du script les déplacent —, puis −.15·l et −.2·l à la place de −.3 et −.6. Mesuré avec ces réglages : formes et mot visibles jusqu'en butée. Avec le collant seul et les facteurs d'origine, le mot sort encore à 310 px (−372 px en butée pour un cadre de 280).animation-timeline: scroll() ou une scroll-timeline nommée : zéro JavaScript, mais rien ne bouge dans Firefox — mesuré dans Firefox 153, CSS.supports('animation-timeline: scroll()') répond faux et les couches de fx-0548 gardent transform: none après défilement — et l'amplitude est figée dans des @keyframes. Ici, un écouteur scroll calcule chaque valeur : l'effet tourne dans Firefox (73° après une molette de 300 unités) et chaque facteur est un nombre dans le script. Côté mouvement : fx-0548, fx-0557 et fx-0573 ne font que translater ; fx-0592 combine translation, scale(1 → 1.3), des rotations de ±5 à 10° et une opacité de .7 à 1 sur cinq couches ; fx-0576 est le seul à tourner une couche de 180° et à faire grossir puis fondre l'avant-plan (×1,8, 50 %). Et le seul dont deux couches sur trois sortent du cadre avant le tiers de la course — voir la question précédente.const au niveau du script (parallaxScroll, plxBg, plxMid, plxFg), sans IIFE, et quatre getElementById — une seule instance possible. Mesuré : le second collage lève cette erreur de syntaxe et le second bloc ne s'exécute pas (le premier, déjà lancé, continue). Pour plusieurs instances : enveloppez le script dans (() => { … })() ou, mieux, remplacez les ids par des classes et bouclez sur document.querySelectorAll('.parallax-viewport'), chaque instance cherchant ses couches par querySelector dans son propre conteneur ; les ids du HTML deviennent inutiles. Le garde parallaxScroll && plxBg && … de l'original ne protège que d'un HTML absent, pas d'un doublon.