Image Zoom on Scroll
An image eases from a close crop out to its full frame as its section passes through the view, drifting up a touch as it goes. It’s tied to the scroll, so it plays backwards too. Swap the values to zoom in instead.
Scroll inside ↓
How to use it
- Paste the snippet just before
</head>. - Wrap the image in an element with
data-image-zoom-on-scrolland give it a size oraspect-ratio. The wrapper stays put and clips the image as it zooms.
Options
Add any of these attributes to the same element.
data-from | Scale as it enters the view. Default 1.4. |
data-to | Scale once it reaches the middle. Default 1. Make it bigger than data-from to zoom in. |
data-rise | How far the image drifts up while zoomed, as a share of the frame. Default 0.08; 0 for none. |
The first element inside zooms: an <img>, <picture>, <video> or anything else. Works inside scrolling boxes too.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Image Zoom on Scroll: an image eases from a close crop out to its full
// frame as its section passes through the view (or the other way round),
// tied to the scroll so it plays forwards and backwards. The frame stays
// put; only the picture inside it moves.
// Usage: <figure data-image-zoom-on-scroll><img src="photo.jpg" alt="…"></figure>
// Optional: data-from (scale as it enters the view, default 1.4), data-to
// (scale as it leaves, default 1; swap them to zoom in instead), data-rise
// (also drift up by this share of the frame's height, default 0.08, for a
// touch of parallax).
// The first child (an <img>, <picture>, <video> or any element) is what
// zooms; the frame clips it, so give the frame a size or an aspect-ratio.
// Works inside scrolling boxes too: it follows the nearest scrolling parent.
var DEFAULTS = { from: 1.4, to: 1, rise: 0.08 };
var css =
'.izs{position:relative;overflow:hidden}' +
'.izs>:first-child{display:block;width:100%;height:100%;object-fit:cover;transform-origin:50% 50%;will-change:transform}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function scroller(el) {
for (var p = el.parentElement; p && p !== document.body; p = p.parentElement) {
var o = getComputedStyle(p).overflowY;
if ((o === 'auto' || o === 'scroll') && p.scrollHeight > p.clientHeight) return p;
}
return null;
}
function num(v, fallback) { return v !== undefined && v !== '' && !isNaN(+v) ? +v : fallback; }
function setup(el) {
var d = el.dataset;
var img = el.firstElementChild;
if (!img) return;
el.classList.add('izs');
var from = num(d.from, DEFAULTS.from), to = num(d.to, DEFAULTS.to), rise = num(d.rise, DEFAULTS.rise);
var box = scroller(el);
var queued = false, last = -1;
function update() {
queued = false;
var r = el.getBoundingClientRect();
var top = 0, h = window.innerHeight;
if (box) { var b = box.getBoundingClientRect(); top = b.top + box.clientTop; h = box.clientHeight; }
// 0 as the frame's top enters at the bottom of the view, 1 as its bottom
// leaves at the top.
var p = (top + h - r.top) / (h + r.height);
p = Math.max(0, Math.min(1, p));
if (Math.abs(p - last) < 0.0005) return;
last = p;
// Ease the zoom so it settles as the frame reaches the middle.
var e = 1 - Math.pow(1 - Math.min(1, p * 1.6), 3);
var s = from + (to - from) * e;
// Rising by more than the zoom allows would show the frame's edge.
var y = (0.5 - p) * 2 * Math.min(rise, Math.abs(s - 1) / 2) * r.height;
anime.utils.set(img, { scale: s, y: y });
}
function onScroll() {
if (queued) return;
queued = true;
requestAnimationFrame(update);
}
(box || window).addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
update();
}
function init() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
document.querySelectorAll('[data-image-zoom-on-scroll]').forEach(function (el) {
if (el._izs) return;
el._izs = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><figure data-image-zoom-on-scroll style="aspect-ratio: 16 / 9">
<img src="lake.jpg" alt="A mountain lake">
</figure>