Scroll✨ Premium

Zoom In Sticky

A 120 px square in a scrolling 280 px box: animation-timeline: scroll() takes it from scale(1) to scale(2.2) and its corner radius from 20 to 4 px over 520 px of travel. In the flow, not pinned despite its name; CSS only.

CSSZoomScroll

Updated

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

The weather balloon swells as it rises — educational page of a meteorological service — Zoom In Sticky example 1

① The weather balloon swells as it rises — educational page of a meteorological service

When“Understanding the atmosphere” page of a weather service: a title, three lines on the pressure dropping to a tenth around 16 km, then an altitude scale fixed on the box (0, 4, 8, 12, 16 km with their pressure); the reader scrolls, the balloon lifts off the ground, climbs through the sky and grows from 96 to 211 px, until it overshoots the 16 km mark through the top; ground, tropopause and burst values pass underneath.
WhyIn the flow, as in the sold code, the object rises with the page and grows all the way: that is the path of a weather balloon, and the 2.2 factor is exact — pressure divided by ten multiplies the volume by ten, the diameter by ∛10 ≈ 2.2. fx-0580 Zoom on Scroll would pin it 110 px from the top: a balloon that does not rise; fx-0563 Zoom & Blur would bring it back to ×1 and blur it at the end of the travel. Here the largest state is the last one and it leaves through the top of the box: the balloon carries on towards 30 km.
SettingsSold code untouched, no position: sticky: 316 px box, stage replaced by a column “178 px intro · 76 px of sky · 96 × 124 px balloon · 76 px of sky · 78 px values”, 216 px travel; balloon as SVG (envelope, string, radiosonde) in a transparent box; gradient sky and altitude scale laid on the box, fixed, while the column scrolls. Measured: envelope centered on the 0 km mark at rest, whole from 95 to 189 px of scrolling (×1.5 to ×2.05), centered on 16 km at the end of the travel at ×2.2 (211 × 273 px), its top 36 px out. The 76 px of sky under the balloon absorb the 74 px that scale(2.2) adds without changing the layout: the final slab does not cover the values.
Zooming into the material of a haute couture fabric — product page of an online shop — Zoom In Sticky example 2

② Zooming into the material of a haute couture fabric — product page of an online shop

WhenPage of a bouclé tweed: fabric name, two lines about the weave, then a 132 px square swatch; the customer scrolls, the square comes to the center and grows to 290 px — weave, cream and navy slubs, overlocked hem and woven label appear; composition, width, weight and price pass underneath.
WhyOn a fabric page, colour is what is sold: fx-0590 grows without touching opacity or blur, the burgundy stays the same from the first to the last pixel of travel — fx-0580 Zoom on Scroll drops to opacity: .8 and greys the material, fx-0563 Zoom & Blur blurs it at both ends. And its 2.2 factor fits the page: 132 px become 290, where fx-0580's 5× would make 660 px. The swatch is not an image: five layers of repeating gradients (45° twill, threads, two dot patterns) that the transform enlarges without blur.
Settings312 px box, column “160 px intro · swatch · 290 px specifications”, 270 px travel; .ss-zoom-box at 132 px, position: sticky; top: 90px (pinned after 70 px), background replaced by the five gradients, hem as outline: 1px dashed, 4 px woven label (8.8 px at 2.2×); 2.2 scale kept, 290 px in 312. Cream background, sans-serif.
The built-up area spreads over sixty years — online atlas of an urban planning agency — Zoom In Sticky example 3

③ The built-up area spreads over sixty years — online atlas of an urban planning agency

When“Urban sprawl” page of an urban planning agency's atlas, on a constant-scale grid (one square = 2 km) laid as the box background: a title, three lines on the built-up footprint gone from 18 to 87 km² while the population only grew ×1.6; the reader scrolls, sixty years go by, the urban footprint — built-up area, old centre, five roads — spreads from 110 to 242 px over a grid that does not move, then the 1960, 1990 and 2020 readings pass underneath.
WhyNothing rises here: scrolling is time, and the object only moves because the page does — what matters is that it spreads over a fixed grid, at constant scale. The sold code's factor is the right one: a surface multiplied by 4.8 is ×2.2 in each direction, and mid-travel (×1.6, 46 km²) lands on 1990. Since the timeline is the box's scroll rather than the clock, the reader goes back to 1960 by scrolling up and stops on whichever year they want. fx-0570 Zoom Into Content would pop the footprint at its final size as soon as it enters; fx-0580 Zoom on Scroll would grey it to .8 — on a map, the tint of the built-up area is a legend and must not change.
SettingsSold code untouched, no position: sticky: 312 px box, column “190 px intro · 56 px of map · 110 × 110 px footprint · 68 px of map · 66 px readings”, 178 px travel; footprint as SVG (polygon area, five roads, old centre) in a transparent box — the animated border-radius 20 → 4 px acts on an invisible box; 40 px grid as the box background, fixed while the column scrolls. Measured: 66 px of the footprint visible below the fold at rest, whole from 70 px of scrolling (×1.47) to the end, 242 px from 2 to 244 at the end of the travel at ×2.2, readings at 246; the 68 px of map under the footprint absorb the 66 px that scale(2.2) adds downwards. Paper background, sans-serif.

How it works

Four elements and two texts: the frame .demo-preview (centered flex, min-height: 300px, #0a0a0f background, white text), the box .sdc-scroll-container (280px, overflow-y: auto, scroll-behavior: smooth), the stage .ss-zoom-content (800 px, centered flex) and the square .ss-zoom-box: 120 × 120 px, border-radius: 20px, 135° gradient #6366f1 → #ec4899, the word ZOOM at 19.2 px weight 800. Two hard-coded texts, white at 30%: the label .ss-zoom-label “Zoom progressif lie au scroll” (11.2 px, 20 px from the bottom of the stage, French with a missing accent) and the hint .sdc-scroll-hint “Scrollez ↓” (10.4 px, pulsing .3 → .7 every 2 s). Travel: 800 − 280 = 520 px. 2 KB of CSS, no JavaScript, no id, no var(--…) variable; pasted into a blank page, white or dark background, 1,280 and 390 px wide: no request, no console error.

One rule makes the effect: animation-name: ssZoomIn with animation-timeline: scroll() — the animation advances with the position of the nearest scrolling ancestor, the box, not with time; animation-duration: auto and animation-range: normal spread the keyframes over the whole travel, linear joins them in a straight line, animation-fill-mode: none returns the base state outside the animation. Two keyframes: scale(1); border-radius: 20px and scale(2.2); border-radius: 4px. Measured: ×1.23 / 16.9 px at 100 px of scrolling, ×1.6 / 12 px at 260, ×2.2 / 4 px at 520 — +0.23 of scale per 100 px, no plateau. The rendered radius is not the one in the code: transform enlarges the corners too, 4 px at 2.2× is 8.8 px on screen — the rounding goes from 17% to 3.3% of the side, from a thumbnail to a slab. The largest state is the last one: nothing comes back down.

The square is centered 400 px from the top of a stage of which the box shows only 280: at rest it sits at 340–460 px, below the fold — the visitor sees a dark background and the hint. Measured: it enters from the bottom at 60 px of scrolling (88 px visible at 130, ×1.3), is whole from 260 (44–236 px, ×1.6) to 300, then, in the flow and not pinned, leaves through the top while growing: 124 px visible at 390, 64 at 460, and at 520 its 264 px span from −252 to 12 px — 12 px of slab at the top of the box, plus the label. The name says sticky, the code holds no position: sticky (FAQ). At 390 px wide, 264 px fit under the 374 px box with no horizontal track. With no id, several copies in one page each drive their own box.

Accessibility

  • prefers-reduced-motion not in the code: measured under emulation, same values (×1 → ×2.2), the hint keeps pulsing. Nothing is hidden and the growth only happens under the reader's gesture (WCAG 2.3.3). To honor it: @media (prefers-reduced-motion: reduce) { .ss-zoom-box, .sdc-scroll-hint { animation: none } } — the square stays at 120 px, radius 20, the state Firefox shows.
  • Keyboard: measured, Tab focuses the box without a tabindex in Chromium and Firefox; Arrow Down moves 40 px in Chromium (+0.09 of scale) and 51 in Firefox, PageDown 285 and 303, End goes to 520. WebKit 26.5 does not focus the box (arrows inert) without tabindex="0": add it, with a visible :focus-visible and an aria-label.
  • Contrast: ZOOM (white, 19.2 px, weight 800 — “large” text, 3:1 threshold) reads 4.5:1 on the blue #6366f1, 4.3:1 in the middle of the gradient, 3.5:1 on the pink #ec4899: passes; bold text under 18.7 px would fail the 4.5:1 threshold on two thirds of the square. Hint and label (white at 30% on #0a0a0f) read 2.6:1 static — the label always, the hint only if its pulse is removed: its opacity .3 → .7 multiplies with the colour's 30%, 1.2:1 to 1.8:1 under the pulse. Decorative, set aria-hidden="true"; to make them readable, raise them to 70% (9.7:1) and remove the pulse, which would bring them back to between 1.8:1 and 5.2:1.
  • Screen readers: the square is a div whose ZOOM text is read as is; the scale change is not announced, and need not be. If it becomes a meaningful image (map, swatch, schematic), give it role="img" and an aria-label, and write the information outside the square too: a screen reader does not zoom by scrolling. Both texts are read in French whatever the page language.

Browser compatibility

CSS only: animation-timeline: scroll(), animation-duration: auto, animation-range (Scroll-driven Animations), transform, animated border-radius, overflow-y: auto. Zero JavaScript, zero dependencies. Chrome and Edge 115+, Safari and iOS 26+: measured in WebKit 26.5, same values as Chromium 151 at every position. Firefox: no version. Measured in Firefox 153: CSS.supports() returns false for the three declarations, which are dropped; the animation lasts 0 s with no fill and the square keeps its base state, transform: none, border-radius: 20px. A Firefox visitor sees a 120 px square, radius 20, crossing the box like any other content — entering from the bottom at 60 px of scrolling, whole from 200 to 340, gone at 460 — then the label. Nothing hidden, nothing overlapping: there is no effect. The sold code holds no @supports; this fallback is the cascade's own, and @supports not (animation-timeline: scroll()) { .ss-zoom-box { animation: none } } would render the same.

Chrome 115+✓ Full
Firefox (all versions)✓ No effect: 120 px square, radius 20, everything visible
Safari 26+✓ Full
Edge 115+✓ Full
Mobile iOS 26+✓ Full (touch scrolling of the box)
Android Chrome 115+✓ Full

Without animation-timeline (Firefox, Chrome and Edge before 115, Safari and iOS before 26): a 120 px square, radius 20, scrolling with the stage, everything visible. Where overflow is not honored (some mail clients), the 800 px stage is shown whole. Without the gradient, no fallback color: white ZOOM on the frame background.

The code

HTML structure to paste into your page (the CSS + JS come with Premium):

index.html — structure
<div class="demo-preview">
  <div class="sdc-scroll-container">
    <div class="ss-zoom-content">
      <div class="ss-zoom-box">…</div>
      <span class="ss-zoom-label">…</span>
    </div>
  </div>
  <span class="sdc-scroll-hint">…</span>
</div>
Unlock the full code — €9.90/mo excl. tax

The full HTML + CSS + JS of Zoom In Sticky, ready to paste — plus all 811 effects. Or €299 once, lifetime access.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Scale factor (CSS — to { transform: scale(2.2) }) 1 → 2.2× (120 → 264 px) Final size = side × factor. Measured with scale(3): 360 px, 60 of them visible at the end of the travel; at 390 px wide, 360 px still fit under the 374 px box. Beyond its width, overflow-y: auto opens a horizontal track: set overflow-x: hidden.
Radius (CSS — border-radius: 20px → 4px) 20 → 4 px (8.8 px rendered at 2.2×) Rendered radius = radius × scale. 0 for true square corners; 20px → 9px for a constant on-screen rounding (measured: 14.5 px halfway, 9 px at the end, i.e. 20 px rendered).
Pinning the object (CSS — position: sticky; top: 80px to add on .ss-zoom-box) absent: the object is in the flow Measured: the square latches at 260 px of scrolling and ends whole inside the box, from 8 to 272 px, instead of 12 px visible (details in the FAQ). The line the fabric scene adds; the balloon and the urban footprint stay in the flow.
Travel (CSS — .ss-zoom-content { height: 800px }, .sdc-scroll-container { height: 280px }) 520 px (+0.23 of scale per 100 px) Travel = stage − box: a 1,200 px stage = 920 px of travel, a zoom twice as slow. Measured with a 480 px box: square visible at rest (340–460), 320 px travel, 212 of its 264 px visible at the end.
Timeline and range (CSS — animation-timeline: scroll(), animation-range: normal) nearest scroller, 0 → 100% of the travel scroll(root) drives the square by the whole page, view() by its own crossing of the viewport. animation-range: 20% 80% concentrates the zoom on the middle 312 px — measured: ×1 at 104, ×2.18 at 410, then back to 120 px from 416 on (animation-fill-mode: none); both holds 2.2× from 416 to 520.
Pacing (CSS — animation-timing-function: linear) linear Measured with ease-in-out: ×1.16 at 130 px (against ×1.3), ×1.6 at 260, ×2.05 at 390 (against ×1.9) — slow start, rush through the middle, gentle landing. ease-out puts most of the zoom in the first third.
Content and texts (CSS — width/height: 120px; HTML — ZOOM, .ss-zoom-label, .sdc-scroll-hint) 120 × 120 px, ZOOM; “Scrollez ↓”, “Zoom progressif lie au scroll” Everything inside the square grows with it: ZOOM is 42 px at 2.2×, a 120 px <img> is stretched to 264 (blurry) — provide a 264 px source, or SVG and gradients like the scenes; 200 × 130 px give 440 × 286. Both texts are hard-coded in French: translate them or delete both spans (sdcHintPulse goes with them; the hint is anchored to the .demo-preview frame).

FAQ

Because the square is centered in an 800 px stage of which the 280 px box shows only the top: at rest it sits at 340–460 px, below the fold, and nothing pins it — it rises with the stage while growing and leaves through the top (measurements above: whole from 260 to 300, 12 px visible at 520). Three fixes: a taller box (measured at 480 px: visible at rest, 212 px visible at the end), content above the square so its entrance reads as an announcement, or the position: sticky line (next question). The trap: moving the square up in the stage and nothing else — it leaves even earlier.
The sold code holds no position: sticky; the name comes from the catalog card. One line is enough: .ss-zoom-box { position: sticky; top: 80px }. Measured in the sold box: the square rises from 340 to 80 px, latches there at 260 px of scrolling (×1.6), then stays centered at 140 px while the stage scrolls underneath; at 520, its 264 px span from 8 to 272 — whole inside the box. The top is computed: wanted center − 60 px (half the side), and the final square must fit in the box (264 px in 280). The sticky containing block is the stage .ss-zoom-content: the square stays pinned inside it to the end, as in the fabric scene on this page.
Same starting picture — gradient square, the word ZOOM, tightening corners, 280 px box — but two codes. fx-0580 computes in JavaScript: a scroll listener reads t = scrollTop ÷ 620 and writes scale(1 + 4t), border-radius: 12 − 10t and opacity: 1 − .2t on a 60 px target, with position: sticky; top: 80px in its CSS: the square latches 110 px from the top and grows in place up to 5×, 300 px, greying to .8; two ids (#zoom-scroll, #zoom-target) — one instance per page; it runs on Firefox. fx-0590 writes nothing: animation-timeline: scroll() animates scale(1) → scale(2.2) and border-radius: 20px → 4px on a 120 px square in the flow, not sticky despite its name, no opacity, no id — as many copies as boxes — and nothing on Firefox. The rule: fx-0580 when the object must stay on screen to become readable (QR, chart, formula: its three scenes), when Firefox matters, or when the page must read t — show a percentage, unlock a button; fx-0590 when the object must cross the box while growing (balloon, urban footprint), keep its opacity and colours, fit in the box at 2.2× (264 px in 280, against 300 for fx-0580's 5×), or exist in several copies — and one position: sticky line gives it pinning back (previous question).