Scroll✨ Premium

Zoom on Scroll

A 60 px square held sticky inside a 280 px box, its scale following the scroll — scale = 1 + 4 × scrollTop ÷ 620 — up to 300 px, while its corner radius goes from 12 to 2 px (10 px on screen once enlarged) and its opacity from 1 to .8. At 5×, the square overshoots the box by 40 px at the top.

JavaScriptCSS TransformScroll

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Scroll has 70 effects, including 12 free. Explore the category →

3 usage examples

Food traceability label on an organic product page — the QR grows until it can be scanned — Zoom on Scroll example 1

① Food traceability label on an organic product page — the QR grows until it can be scanned

WhenProduct page of a farm selling direct: the “Origin” section shows a traceability QR as a small rounded thumbnail; scrolling toward it, the code fills the column and its corners sharpen, the customer scans it from their phone with no button and no window.
WhyThe 1 → 5× progression indexed on scrollTop lets the customer pick the moment the QR reaches a scannable size — no “enlarge” button, no modal, the scroll gesture is enough and stays within the reading of the page. The white rounded square reads as a thumbnail; at 5×, it is a label.
SettingsSold code untouched (same ids, same script): 326 px box, top: 128px to center the square (the original top: 80px would clip 40 px), white background and a 4 px margin around a 25-module SVG QR (2.1 px per module at rest, 10.4 px at 5×); 574 px travel, 143 px per unit of scale. The final .8 opacity comes from the code: the QR stays readable, the white turns light grey.
Key infographic in an online investigative article — the chart opens up for close reading — Zoom on Scroll example 2

② Key infographic in an online investigative article — the chart opens up for close reading

WhenDigital news article: a summary curve (public debt as a share of GDP) sits as a compact thumbnail in the flow of the text; scrolling to it, it grows until it fills the window and lets the axes and annotations be read.
WhyThe enlargement follows the scroll, hence the attention: the reader who slows down on the chart sees it grow, the one who passes leaves it as a thumbnail. A click opening a modal would break the reading rhythm; here the zoom is proportional to attention, and scrolling back closes it.
Settings96 × 60 px target (480 × 300 at 5×) in a 320 px box, top: 126px; chart in SVG, whose 3.4 px texts become 17 px; opacity fixed at 1 — the code's 1 − .2t greyed the axes; own instance (#inv-scroll) with the same math: the sold code drives a single id.
Reaction equation in a university organic chemistry course — the formula goes from reference to centerpiece — Zoom on Scroll example 3

③ Reaction equation in a university organic chemistry course — the formula goes from reference to centerpiece

WhenOnline university course: the Fischer esterification equation sits as a compact card in the middle of a section; the student scrolls, the card grows until it fills the whole reading area and its corners fade away.
WhyLosing the radius while zooming encodes a change of status — one card among others, then a slab filling the frame — with no annotation or tooltip. No other effect in the category couples a changing geometric shape with a zoom driven by the scroll position.
Settings78 × 34 px target (390 × 170 at 5×) in a 400 × 320 px area, top: 139px; radius pushed to zero with 12 − 12t (the code stops at 2 px, 10 px rendered); formula in SVG, subscripts as Unicode characters; own instance (#chem-scroll).

How it works

The sold HTML is four elements: the frame .demo-preview (centered flex, min-height: 300px, position: relative, #0a0a0f background), the scrolling box .scroll-container#zoom-scroll (100% × 280px, overflow: hidden auto, scroll-behavior: smooth, a 4 px scrollbar styled with ::-webkit-scrollbar), the stage .zoom-inner (flex, justify-content: center, align-items: flex-start, 900 px tall, padding-top: 100px) and the target .zoom-target#zoom-target: 60 × 60 px, 135° gradient #6366f1 → #a855f7 → #ec4899, border-radius: 12px, position: sticky; top: 80px, the word ZOOM at 11.2 px weight 800, and transition: transform .1s linear, border-radius .3s. Below the box, the label .scroll-hint-label (absolute, 8 px from the bottom of the frame, 10.4 px, white at 30%) pulses between .3 and .7 opacity every 2 s; its text “Scrollez pour explorer” is hard-coded in French in the HTML. The travel is 900 − 280 = 620 px. No inline style, no site CSS variable, no console error.

The script (10 lines) attaches a single scroll listener to the box. On each event: t = scrollTop ÷ (scrollHeight − clientHeight), i.e. scrollTop ÷ 620, then three writes to styletransform: scale(1 + 4t), border-radius: max(12 − 10t, 0), opacity: 1 − .2t. One variable, three outputs, 155 px of travel per unit of scale. Measured in 62 px steps: 1.4 / 11 px / .98 at the first step, 3 / 7 px / .9 halfway, 5 / 2 px / .8 at the bottom. The radius never reaches zero: 12 − 10t is 2 px at t = 1 (the Math.max(…, 0) never kicks in), and since transform enlarges the corners too, the rendered corner measures 10 px at 5× — the “slab” keeps a rounding of 3% of its side, against 20% at rest. The two transitions differ in length: after a 245 px jump (PageDown), the scale reaches its value in 104 ms, the radius takes 300 ms — when the size settles, the corner still shows 6.2 px (31 px rendered) and finishes tightening 200 ms later.

The target starts 100 px from the top (the padding-top), slides 20 px, then sticky; top: 80px pins it: its center stays 110 px from the top of the box for the remaining 600 px, and the transform grows around that center (transform-origin 30 px 30 px). At 5×, the 300 px square spans from −40 to 260 px: 40 px are clipped at the top (13% of the square) by overflow: hidden, 20 px stay free at the bottom — measured identical at 320, 800 and 1,440 px wide; width only changes the side margins (2 px each in a 304 px box). scroll-behavior: smooth does nothing to wheel notches in Chromium (a 100 px notch = 2 events in 114 ms) but animates script-set positions: scrollTop = 620 reads back 0 immediately, lands at 408 ms, and the scale finishes at 508 ms. The listener lives on the box, nothing global to remove; but a top-level const zoomScroll plus getElementById limit the code to one instance per page — pasted twice: Identifier 'zoomScroll' has already been declared, second copy inert (measured: transform: none after 620 px of scrolling).

Accessibility

  • prefers-reduced-motion not in the code: measured under emulation, the scale reaches 5 at the bottom, the label keeps pulsing and the 100 and 300 ms transitions remain. The growth is driven by the reader (nothing moves without them, except the label), which makes it an interaction-triggered animation (WCAG 2.3.3) — add @media (prefers-reduced-motion: reduce) { .zoom-target { transition: none } .scroll-hint-label { animation: none } .scroll-container { scroll-behavior: auto } }, and if the square's content is readable at rest, cap the scale (1 + 2t) behind matchMedia.
  • Keyboard: measured in Chromium, Tab focuses the box without a tabindex (outline: auto 1px ring), arrow down moves 40 px (+0.26 of scale), PageDown or Space 245 px (+1.58), End goes all the way (5×). Firefox and Safari do not focus a scrolling box without tabindex="0" on #zoom-scroll: add it, with a visible :focus-visible. Wheel and nested touch scrolling work; on a touch screen, the gesture must start inside the box.
  • Contrast and legibility: the word ZOOM (white, weight 800) measures 4.5:1 on the blue, 4.0:1 on the purple and 3.5:1 on the pink of the gradient — below 4.5:1 on two thirds of the square, at 11.2 px. At 5× it is 56 px (3:1 threshold) but opacity drops to .8: text and background both blend toward #0a0a0f, 3.9:1 / 3.6:1 / 3.2:1 — a narrow pass on the pink. If the square carries real content (QR, chart, formula), keep opacity: 1 and fix the top: at maximum, the top 40 px are outside the frame, hence unreadable. The label pulses between 2.6:1 and 9.8:1: an instruction hard to read half of the time.
  • Screen readers and no JavaScript: the target is a div whose text is read as is; the scale change is not announced (nothing to announce if it is decorative: set aria-hidden="true"). If the square holds information, it must exist at a readable size without scrolling — a screen reader does not “zoom” by scrolling. The label “Scrollez pour explorer” is read in French whatever the page language. Without the script, the box scrolls, the square slides from 100 to 80 px then stays pinned at 60 px, radius 12: nothing grows, only the label pulses.

Browser compatibility

ES2015 script (const, arrow function, `scale(${e})` template), position: sticky, transition and a gradient. A single recent property: the two-value form overflow: hidden auto — a browser that does not understand it drops the declaration, the box stretches and nothing scrolls. No animation-timeline or scroll-timeline: the scale comes from a classic scroll listener writing a transform, so the effect works on Firefox, unlike its neighbours driven by native CSS scroll animations (fx-0590, fx-0570). Zero dependencies.

Chrome 68+✓ Full
Firefox 61+✓ Full (native scrollbar, ::-webkit-scrollbar ignored)
Safari 13+✓ Full (smooth scrolling ignored before 15.4)
Edge 79+✓ Full
Mobile iOS 13+✓ Full (touch scrolling of the box)
Android Chrome✓ Full

Without JavaScript, the box scrolls, the square slides 20 px then stays pinned at 60 px, radius 12, opacity 1: nothing grows, only the label pulses. Without overflow: hidden auto (Chrome < 68, Firefox < 61, Safari < 12.1), the box is 900 px tall and nothing scrolls. Without position: sticky, the square rises with the content while growing and leaves through the top.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<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>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Scale factor (JS — e = 1 + 4 * t) 1 → 5× (60 → 300 px) Final size = side × factor. In a 280 px box, 5× does not fit (300 px): 4.6× is the largest that can be centered (1 + 3.6 * t). 1 + 1 * t gives a discreet 2×, read as a highlight.
Radius (JS — r = 12 - 10 * t, Math.max(r, 0)) 12 → 2 px (10 px rendered at 5×) The transform enlarges the corners too: rendered radius = radius × scale. For true square corners, 12 − 12 * t; for a constant on-screen rounding, 12 / e. The Math.max only matters if the slope exceeds 12.
Opacity (JS — 1 - .2 * t) 1 → .8 The slab becomes 20% translucent at the end of the travel and its white text turns grey (#cecece on a dark background). Set it to 1 for content meant to be read, or invert it (.6 + .4 * t) to make the square emerge.
Travel (CSS — .zoom-inner { height: 900px }, .scroll-container { height: 280px }) 620 px (155 px per unit) Travel = stage height − box height, read back by the script (scrollHeight − clientHeight): nothing to change in JS. A 1,500 px stage = 1,220 px of travel, 305 px per unit, a slower zoom; the stage is also the sticky containing block, the target stays pinned inside it to the end.
Pin point (CSS — .zoom-target { top: 80px }) 80 px (center at 110 px, 40 px clipped at 5×) The square's center pins at top + 30 px. To center it in the box: top = height ÷ 2 − 30, i.e. 110 px for 280 px. The final square must fit in the box: 300 px require a box of at least 300 px (326 px and top: 128px in the first scene).
Size and content (CSS — width/height: 60px, HTML — the ZOOM text) 60 × 60 px, the word ZOOM at 11.2 px Everything inside grows with the box: 11.2 px text becomes 56 px at 5×, a 60 px <img> is stretched to 300 px (blurry) — provide a 300 px source displayed at 60 px, or SVG (QR, chart, formula). The square is not mandatory: 96 × 60 px give 480 × 300.
Responsiveness (CSS — transition: transform .1s linear, border-radius .3s) 100 ms / 300 ms Lag between the scroll and the size: at 0 the square sticks to the gesture, at .3s it floats. The radius's 300 ms make it trail 200 ms behind the size (31 px rendered when the scale settles, 10 px afterwards): align both on .1s.
Smooth scrolling and label (CSS — scroll-behavior: smooth, .scroll-hint-label) smooth / “Scrollez pour explorer” pulsing 2 s smooth animates script-set positions (scrollTop = 620: reads back 0, reached at 408 ms) — switch to auto if a button or anchor drives the box. The label is a catalog leftover: hard-coded French, absolute relative to .demo-preview (without that frame it drops to the bottom of the page), two lines below 341 px wide.

FAQ

Because the target is pinned at top: 80px: its center settles 110 px from the top of a 280 px box, and the transform grows around that center. At 5×, 300 px span from −40 to 260 px: the top 40 px leave the box, which overflow: hidden auto clips, while 20 px stay free at the bottom. Measured identical at 320, 800 and 1,440 px wide — width changes nothing. Two fixes: center with top: 110px and cap at 4.6× (1 + 3.6 * t, 276 px), or keep 5× in a box of at least 300 px (height: 326px, top: 128px). The three scenes on this page center the target and size the box so the final slab fits whole.
No. The script writes 12 − 10t, hence 2 px at t = 1 — the Math.max(r, 0) never kicks in, the value never drops below 2. And since transform: scale() enlarges the box with its corners, the rendered radius is radius × scale: 12 px at rest, 10 px at 5×. As a share of the side, the rounding goes from 20% to 3.3% — that shift is what gives the slab impression, not a right angle. For true square corners, write 12 − 12t; for a constant on-screen rounding, 12 / e. Note also that the radius transition lasts 300 ms against 100 ms for the scale: after a PageDown, the corner is still at 6.2 px (31 px rendered) when the size has already settled, and finishes tightening 200 ms later.
The starting look is similar — gradient square, the word ZOOM, tightening corners — but fx-0590 is written in native CSS: animation-timeline: scroll() animates scale(1) → scale(2.2) and border-radius: 20px → 4px on a 120 px box placed in the middle of an 800 px stage, not sticky despite its name and with an empty JavaScript field. Chrome 115+, Edge 115+, Safari 26; nothing on Firefox, which does not enable scroll-driven animations. fx-0580 does the math in JavaScript: a scroll listener, a variable t, a transform written on every event — it runs on Firefox 61+, the target stays pinned at 110 px while the stage scrolls behind it, the factor goes to 5× instead of 2.2 and the opacity drops to .8. Above all, t is a number the rest of the page can read: show a percentage, unlock a button, change a text — out of reach for a CSS animation.