<div class="background">
    <div class="container">
        <div class="image">
            <img src="https://picsum.photos/1080/1080?1" data-scroll-zoom />
        </div>
        <div class="image">
            <img src="https://picsum.photos/1080/1080?2" data-scroll-zoom />
        </div>
        <div class="image">
            <img src="https://picsum.photos/1080/1080?3" data-scroll-zoom />
        </div>
        <div class="image">
            <img src="https://picsum.photos/1080/1080?4" data-scroll-zoom />
        </div>
        <div class="image">
            <img src="https://picsum.photos/1080/1080?5" data-scroll-zoom />
        </div>
        <div class="image">
            <img src="https://picsum.photos/1080/1080?6" data-scroll-zoom />
        </div>
        <div>
          <p>This method utilizes the Intersection Observer to zoom the image <em>only when it is in view,</em> which cuts down on the processing power required for this effect.</p>
        </div>
    </div>
</div>
body {
  margin: 0;
}

.background {
  align-items: center;
  background: #04FEBA;
  display: flex;
  height: 100%;
  justify-content: center;
  width: 100vw;
}

.container {
  align-items: center;
  display: flex;
  flex-direction: column;
  height: 600vh;
  justify-content: space-around;
  text-align: center;
  text-transform: uppercase;
  width: 100vmin;
  
  p {
    font-family: Merriweather, sans-serif;
    font-size: 20px;
    line-height: 1.5;
    max-width: 70vmin;
    text-align: justify;
    text-transform: none;
  }
}

.image {
  background: white;
  box-shadow: 3px 10px 10px rgba(0, 0, 0, 0.25);
  border: 15px solid white;
  border-width: 1vmin 1vmin 10vmin 1vmin;
  height: 70vmin;
  overflow: hidden;
  width: 70vmin;
  
  img {
    height: 100%;
    object-fit: cover;
    width: 100%;
  }
}
View Compiled
// Higher number = more zoom
let scaleAmount = 0.5;

function scrollZoom() {
  const images = document.querySelectorAll("[data-scroll-zoom]");
  let scrollPosY = 0;
  scaleAmount = scaleAmount / 100;

  const observerConfig = {
    rootMargin: "0% 0% 0% 0%",
    threshold: 0
  };

  // Create separate IntersectionObservers and scroll event listeners for each image so that we can individually apply the scale only if the image is visible
  images.forEach(image => {
    let isVisible = false;
    const observer = new IntersectionObserver((elements, self) => {
      elements.forEach(element => {
        isVisible = element.isIntersecting;
      });
    }, observerConfig);

    observer.observe(image);

    // Set initial image scale on page load
    image.style.transform = `scale(${1 + scaleAmount * percentageSeen(image)})`;

    // Only fires if IntersectionObserver is intersecting
    window.addEventListener("scroll", () => {
      if (isVisible) {
        scrollPosY = window.pageYOffset;
        image.style.transform = `scale(${1 +
          scaleAmount * percentageSeen(image)})`;
      }
    });
  });

  // Calculates the "percentage seen" based on when the image first enters the screen until the moment it leaves
  // Here, we get the parent node position/height instead of the image since it's in a container that has a border, but
  // if your container has no extra height, you can simply get the image position/height
  function percentageSeen(element) {
    const parent = element.parentNode;
    const viewportHeight = window.innerHeight;
    const scrollY = window.scrollY;
    const elPosY = parent.getBoundingClientRect().top + scrollY;
    const borderHeight = parseFloat(getComputedStyle(parent).getPropertyValue('border-bottom-width')) + parseFloat(getComputedStyle(element).getPropertyValue('border-top-width'));
    const elHeight = parent.offsetHeight + borderHeight;

    if (elPosY > scrollY + viewportHeight) {
      // If we haven't reached the image yet
      return 0;
    } else if (elPosY + elHeight < scrollY) {
      // If we've completely scrolled past the image
      return 100;
    } else {
      // When the image is in the viewport
      const distance = scrollY + viewportHeight - elPosY;
      let percentage = distance / ((viewportHeight + elHeight) / 100);
      percentage = Math.round(percentage);

      return percentage;
    }
  }
}

scrollZoom();

External CSS

  1. https://fonts.googleapis.com/css?family=Merriweather&amp;display=swap

External JavaScript

This Pen doesn't use any external JavaScript resources.