Zoom on Scroll
Un carré de 60 px collé (sticky) dans un bloc de 280 px, dont l'échelle suit le défilement — scale = 1 + 4 × scrollTop ÷ 620 — jusqu'à 300 px, pendant que son rayon passe de 12 à 2 px (10 px à l'écran une fois grossi) et son opacité de 1 à .8. À 5×, le carré dépasse le bloc de 40 px par le haut.
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 HTML vendu tient en quatre éléments : le cadre .demo-preview (flex centré, min-height: 300px, position: relative, fond #0a0a0f), le bloc défilant .scroll-container#zoom-scroll (100% × 280px, overflow: hidden auto, scroll-behavior: smooth, barre de 4 px stylée par ::-webkit-scrollbar), le plateau .zoom-inner (flex, justify-content: center, align-items: flex-start, 900 px de haut, padding-top: 100px) et la cible .zoom-target#zoom-target : 60 × 60 px, dégradé 135° #6366f1 → #a855f7 → #ec4899, border-radius: 12px, position: sticky; top: 80px, le mot ZOOM en 11,2 px graisse 800, et transition: transform .1s linear, border-radius .3s. Sous le bloc, l'étiquette .scroll-hint-label (absolue, à 8 px du bas du cadre, 10,4 px, blanc à 30 %) pulse entre .3 et .7 d'opacité toutes les 2 s ; son texte « Scrollez pour explorer » est écrit en dur dans le HTML. La course vaut 900 − 280 = 620 px. Aucun style en ligne, aucune variable CSS du site, aucune erreur console.
Le script (10 lignes) pose un seul écouteur scroll sur le bloc. À chaque événement : t = scrollTop ÷ (scrollHeight − clientHeight), soit scrollTop ÷ 620, puis trois écritures dans style — transform: scale(1 + 4t), border-radius: max(12 − 10t, 0), opacity: 1 − .2t. Une variable, trois sorties, 155 px de course par unité d'échelle. Mesuré par crans de 62 px : 1,4 / 11 px / .98 au premier cran, 3 / 7 px / .9 à mi-course, 5 / 2 px / .8 en bas. Le rayon ne descend jamais à zéro : 12 − 10t vaut 2 px à t = 1 (le Math.max(…, 0) ne sert jamais), et comme transform agrandit aussi les coins, l'angle rendu mesure 10 px à 5× — la « dalle » garde un arrondi de 3 % du côté, contre 20 % au repos. Les deux transitions n'ont pas la même durée : après un saut de 245 px (PageDown), l'échelle atteint sa valeur en 104 ms, le rayon met 300 ms — au moment où la taille se fige, le coin affiche encore 6,2 px (31 px rendus) et finit de se resserrer 200 ms plus tard.
La cible part à 100 px du haut (le padding-top), glisse de 20 px, puis sticky; top: 80px la fixe : son centre reste à 110 px du haut du bloc pendant les 600 px restants, et le transform grossit autour de ce centre (transform-origin 30 px 30 px). À 5×, le carré de 300 px s'étend de −40 à 260 px : 40 px sont rognés par le haut (13 % du carré) par overflow: hidden, 20 px restent libres en bas — mesuré identique à 320, 800 et 1 440 px de large ; la largeur ne change que les marges latérales (2 px de chaque côté dans un bloc de 304 px). Le scroll-behavior: smooth n'agit pas sur la molette dans Chromium (un cran de 100 px = 2 événements en 114 ms) mais anime les positions posées par script : scrollTop = 620 se relit 0 immédiatement, arrive à 408 ms, et l'échelle finit à 508 ms. L'écouteur vit sur le bloc, rien de global à retirer ; en revanche const zoomScroll au niveau global et getElementById limitent le code à une instance par page — collé deux fois : Identifier 'zoomScroll' has already been declared, seconde copie inerte (mesuré : transform: none après 620 px de défilement).
Accessibilité
- prefers-reduced-motion absent du code : mesuré sous émulation, l'échelle atteint 5 en bas de course, l'étiquette pulse toujours et les transitions de 100 et 300 ms restent. Le grossissement est piloté par le lecteur (rien ne bouge sans lui, hormis l'étiquette), ce qui le range parmi les animations déclenchées par l'interaction (WCAG 2.3.3) — ajoutez
@media (prefers-reduced-motion: reduce) { .zoom-target { transition: none } .scroll-hint-label { animation: none } .scroll-container { scroll-behavior: auto } }, et si le contenu du carré est lisible au repos, plafonnez l'échelle (1 + 2t) derrièrematchMedia. - Clavier : mesuré dans Chromium, Tab donne le focus au bloc sans
tabindex(anneauoutline: auto 1px), flèche bas avance de 40 px (+0,26 d'échelle), PageDown ou Espace de 245 px (+1,58), Fin va au bout (5×). Firefox et Safari ne focalisent pas un bloc défilant sanstabindex="0"sur#zoom-scroll: ajoutez-le avec un:focus-visiblevisible. Molette et défilement tactile imbriqué fonctionnent ; sur écran tactile, le geste doit commencer dans le bloc. - Contraste et lisibilité : le mot ZOOM (blanc, graisse 800) mesure 4,5:1 sur le bleu, 4,0:1 sur le violet et 3,5:1 sur le rose du dégradé — sous 4,5:1 sur les deux tiers du carré, à 11,2 px. À 5×, il fait 56 px (seuil 3:1) mais l'opacité tombe à .8 : texte et fond se fondent vers
#0a0a0f, 3,9:1 / 3,6:1 / 3,2:1 — tenu de peu sur le rose. Si le carré porte un vrai contenu (QR, graphique, formule), gardezopacity: 1et corrigez letop: au maximum, les 40 px du haut sont hors cadre, donc illisibles. L'étiquette pulse entre 2,6:1 et 9,8:1 : consigne peu lisible la moitié du temps. - Lecteurs d'écran et sans JavaScript : la cible est un
divdont le texte est lu tel quel ; le changement d'échelle n'est pas annoncé (rien à annoncer s'il est décoratif : posezaria-hidden="true"). Si le carré contient une information, elle doit exister à taille lisible sans défiler — un lecteur d'écran ne « zoome » pas par le défilement. L'étiquette « Scrollez pour explorer » est lue en français quelle que soit la langue de la page. Sans script, le bloc défile, le carré glisse de 100 à 80 px puis reste collé à 60 px, rayon 12 : rien ne grossit, seule l'étiquette pulse.
Compatibilité navigateur
Script ES2015 (const, fonction fléchée, gabarit `scale(${e})`), position: sticky, transition et un dégradé. Une seule propriété récente : la forme à deux valeurs overflow: hidden auto — un navigateur qui ne la comprend pas ignore la déclaration, le bloc s'étire et rien ne défile. Pas d'animation-timeline ni de scroll-timeline : l'échelle vient d'un écouteur scroll classique qui écrit un transform, l'effet fonctionne donc sur Firefox, contrairement aux voisins pilotés au scroll en CSS natif (fx-0590, fx-0570). Zéro dépendance.
Sans JavaScript, le bloc défile, le carré glisse de 20 px puis reste collé à 60 px, rayon 12, opacité 1 : rien ne grossit, seule l'étiquette pulse. Sans overflow: hidden auto (Chrome < 68, Firefox < 61, Safari < 12.1), le bloc mesure 900 px et rien ne défile. Sans position: sticky, le carré remonte avec le contenu tout en grossissant et sort par le haut.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
<div class="demo-preview">
<div class="scroll-container" id="zoom-scroll">
<div class="zoom-inner">
<div class="zoom-target" id="zoom-target">…</div>
</div>
</div>
<span class="scroll-hint-label">…</span>
</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 |
|---|---|---|
Facteur d'échelle (JS — e = 1 + 4 * t) |
1 → 5× (60 → 300 px) | Taille finale = côté × facteur. Dans un bloc de 280 px, 5× ne tient pas (300 px) : 4,6× est le maximum centrable (1 + 3.6 * t). 1 + 1 * t donne un discret 2×, lisible comme une mise en avant. |
Rayon (JS — r = 12 - 10 * t, Math.max(r, 0)) |
12 → 2 px (10 px rendus à 5×) | Le transform agrandit aussi les coins : rayon rendu = rayon × échelle. Pour de vrais angles droits, 12 − 12 * t ; pour un arrondi constant à l'écran, 12 / e. Le Math.max ne sert que si la pente dépasse 12. |
Opacité (JS — 1 - .2 * t) |
1 → .8 | La dalle devient translucide à 20 % en bout de course et son texte blanc vire au gris (#cecece sur fond sombre). Passez à 1 pour un contenu à lire, ou inversez (.6 + .4 * t) pour faire émerger le carré. |
Course (CSS — .zoom-inner { height: 900px }, .scroll-container { height: 280px }) |
620 px (155 px par unité) | La course vaut hauteur du plateau − hauteur du bloc, relue par le script (scrollHeight − clientHeight) : rien à changer en JS. 1 500 px de plateau = 1 220 px de course, 305 px par unité, zoom plus lent ; le plateau est aussi le bloc conteneur du sticky, la cible y reste collée jusqu'au bout. |
Point de fixation (CSS — .zoom-target { top: 80px }) |
80 px (centre à 110 px, 40 px rognés à 5×) | Le centre du carré se fixe à top + 30 px. Pour le centrer dans le bloc : top = hauteur ÷ 2 − 30, soit 110 px pour 280 px. Le carré final doit tenir dans le bloc : 300 px exigent un bloc d'au moins 300 px (326 px et top: 128px dans la première scène). |
Taille et contenu (CSS — width/height: 60px, HTML — le texte ZOOM) |
60 × 60 px, mot ZOOM 11,2 px | Tout contenu est agrandi avec la boîte : un texte de 11,2 px fait 56 px à 5×, un <img> de 60 px est étiré à 300 px (flou) — fournissez une source de 300 px affichée en 60 px, ou du SVG (QR, graphique, formule). Le carré n'est pas obligatoire : 96 × 60 px donnent 480 × 300. |
Réactivité (CSS — transition: transform .1s linear, border-radius .3s) |
100 ms / 300 ms | Retard entre le défilement et la taille : à 0 le carré colle au geste, à .3s il flotte. Les 300 ms du rayon le font traîner 200 ms derrière la taille (31 px rendus au moment où l'échelle se fige, 10 px ensuite) : alignez les deux sur .1s. |
Défilement doux et étiquette (CSS — scroll-behavior: smooth, .scroll-hint-label) |
smooth / « Scrollez pour explorer » pulsant 2 s | Le smooth anime les positions posées par script (scrollTop = 620 : relu 0, atteint en 408 ms) — passez à auto si un bouton ou une ancre pilote le bloc. L'étiquette est un reste du catalogue : française en dur, absolue par rapport à .demo-preview (sans ce cadre elle tombe au bas de la page), sur deux lignes sous 341 px de large. |
FAQ
top: 80px : son centre se fixe à 110 px du haut d'un bloc de 280 px, et le transform grossit autour de ce centre. À 5×, 300 px s'étendent de −40 à 260 px : les 40 px du haut sortent du bloc, que overflow: hidden auto coupe, alors que 20 px restent libres en bas. Mesuré identique à 320, 800 et 1 440 px de large — la largeur n'y change rien. Deux réglages : centrer avec top: 110px et plafonner à 4,6× (1 + 3.6 * t, 276 px), ou garder 5× dans un bloc d'au moins 300 px (height: 326px, top: 128px). Les trois scènes de cette page centrent la cible et dimensionnent le bloc pour que la dalle finale tienne entière.12 − 10t, donc 2 px à t = 1 — le Math.max(r, 0) ne sert jamais, la valeur ne descend pas sous 2. Et comme transform: scale() agrandit la boîte avec ses coins, le rayon rendu vaut rayon × échelle : 12 px au repos, 10 px à 5×. En proportion du côté, l'arrondi passe de 20 % à 3,3 % — c'est ce passage qui donne l'impression de dalle, pas un angle droit. Pour de vrais angles droits, écrivez 12 − 12t ; pour un arrondi constant à l'écran, 12 / e. Notez aussi que la transition du rayon dure 300 ms contre 100 ms pour l'échelle : après un PageDown, le coin est encore à 6,2 px (31 px rendus) quand la taille est déjà figée, et finit de se resserrer 200 ms plus tard.animation-timeline: scroll() anime scale(1) → scale(2.2) et border-radius: 20px → 4px sur une boîte de 120 px posée au milieu d'un plateau de 800 px, sans sticky malgré son nom et avec un champ JavaScript vide. Chrome 115+, Edge 115+, Safari 26 ; rien sur Firefox, qui n'active pas les animations pilotées au scroll. fx-0580 fait le calcul en JavaScript : un écouteur scroll, une variable t, un transform écrit à chaque événement — ça tourne sur Firefox 61+, la cible reste collée à 110 px pendant que le plateau défile derrière elle, le facteur monte à 5× au lieu de 2,2 et l'opacité baisse à .8. Surtout, t est un nombre que le reste de la page peut lire : afficher un pourcentage, débloquer un bouton, changer un texte — hors de portée d'une animation CSS.