← Scroll

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

  1. Paste the snippet just before </head>.
  2. Wrap the image in an element with data-image-zoom-on-scroll and give it a size or aspect-ratio. The wrapper stays put and clips the image as it zooms.

Options

Add any of these attributes to the same element.

data-fromScale as it enters the view. Default 1.4.
data-toScale once it reaches the middle. Default 1. Make it bigger than data-from to zoom in.
data-riseHow 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.

1 · Snippet
<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>
2 · Your HTML
<figure data-image-zoom-on-scroll style="aspect-ratio: 16 / 9">
  <img src="lake.jpg" alt="A mountain lake">
</figure>