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.
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



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
tabindexin 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) withouttabindex="0": add it, with a visible:focus-visibleand anaria-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: itsopacity.3 → .7 multiplies with the colour's 30%, 1.2:1 to 1.8:1 under the pulse. Decorative, setaria-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
divwhose 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 itrole="img"and anaria-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.
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):
<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>
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 / property | Default | Effect |
|---|---|---|
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
position: sticky line (next question). The trap: moving the square up in the stage and nothing else — it leaves even earlier.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.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).