<!-- The `.container` element will contain all the images -->
<!-- It will be used also to perform the 'custom scroll' behavior -->
<div class="container">
  <!-- Each following `div` correspond to one image -->
  <!-- The images will be set using CSS backgrounds -->
  <div class="image vh-fix"></div>
  <div class="image vh-fix"></div>
  <div class="image vh-fix"></div>
  <div class="image vh-fix"></div>
  <div class="image vh-fix"></div>
  <div class="image vh-fix"></div>
  <div class="image vh-fix"></div>
  <div class="image vh-fix"></div>
  <div class="image vh-fix"></div>
  <div class="image vh-fix"></div>
</div>
body {
  // `overflow-x` should be hidden, so horizontal scrollbar
  // don't appears when performing transformations
  overflow-x: hidden;

  // Setup the background
  height: 100vh;
  background-color: #9f9eac;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 1600 800'%3E%3Cg %3E%3Cpath fill='%23aeadbc' d='M486 705.8c-109.3-21.8-223.4-32.2-335.3-19.4C99.5 692.1 49 703 0 719.8V800h843.8c-115.9-33.2-230.8-68.1-347.6-92.2C492.8 707.1 489.4 706.5 486 705.8z'/%3E%3Cpath fill='%23bdbccc' d='M1600 0H0v719.8c49-16.8 99.5-27.8 150.7-33.5c111.9-12.7 226-2.4 335.3 19.4c3.4 0.7 6.8 1.4 10.2 2c116.8 24 231.7 59 347.6 92.2H1600V0z'/%3E%3Cpath fill='%23cdccdd' d='M478.4 581c3.2 0.8 6.4 1.7 9.5 2.5c196.2 52.5 388.7 133.5 593.5 176.6c174.2 36.6 349.5 29.2 518.6-10.2V0H0v574.9c52.3-17.6 106.5-27.7 161.1-30.9C268.4 537.4 375.7 554.2 478.4 581z'/%3E%3Cpath fill='%23dcdbee' d='M0 0v429.4c55.6-18.4 113.5-27.3 171.4-27.7c102.8-0.8 203.2 22.7 299.3 54.5c3 1 5.9 2 8.9 3c183.6 62 365.7 146.1 562.4 192.1c186.7 43.7 376.3 34.4 557.9-12.6V0H0z'/%3E%3Cpath fill='%23ecebff' d='M181.8 259.4c98.2 6 191.9 35.2 281.3 72.1c2.8 1.1 5.5 2.3 8.3 3.4c171 71.6 342.7 158.5 531.3 207.7c198.8 51.8 403.4 40.8 597.3-14.8V0H0v283.2C59 263.6 120.6 255.7 181.8 259.4z'/%3E%3Cpath fill='%23dcdbee' d='M1600 0H0v136.3c62.3-20.9 127.7-27.5 192.2-19.2c93.6 12.1 180.5 47.7 263.3 89.6c2.6 1.3 5.1 2.6 7.7 3.9c158.4 81.1 319.7 170.9 500.3 223.2c210.5 61 430.8 49 636.6-16.6V0z'/%3E%3Cpath fill='%23cdccdd' d='M454.9 86.3C600.7 177 751.6 269.3 924.1 325c208.6 67.4 431.3 60.8 637.9-5.3c12.8-4.1 25.4-8.4 38.1-12.9V0H288.1c56 21.3 108.7 50.6 159.7 82C450.2 83.4 452.5 84.9 454.9 86.3z'/%3E%3Cpath fill='%23bdbccc' d='M1600 0H498c118.1 85.8 243.5 164.5 386.8 216.2c191.8 69.2 400 74.7 595 21.1c40.8-11.2 81.1-25.2 120.3-41.7V0z'/%3E%3Cpath fill='%23aeadbc' d='M1397.5 154.8c47.2-10.6 93.6-25.3 138.6-43.8c21.7-8.9 43-18.8 63.9-29.5V0H643.4c62.9 41.7 129.7 78.2 202.1 107.4C1020.4 178.1 1214.2 196.1 1397.5 154.8z'/%3E%3Cpath fill='%239f9eac' d='M1315.3 72.4c75.3-12.6 148.9-37.1 216.8-72.4h-723C966.8 71 1144.7 101 1315.3 72.4z'/%3E%3C/g%3E%3C/svg%3E");
  /* background by SVGBackgrounds.com */
  background-attachment: fixed;
  background-position: center;
  background-size: cover;
}

// The styles for a `div` element (inserted with Javascript)
// Used to make the page scrollable
// Will be setted a proper `height` value using Javascript
.fake-scroll {
  position: absolute;
  top: 0;
  width: 1px;
}

// The container for all images
.container {
  // 2 columns grid
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-gap: 0 10%;
  justify-items: end; // This will align all items (images) to the right

  // Fixed positioned, so it won't be affected by default scroll
  // It will be moved using `transform`, to achieve a custom scroll behavior
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
}

// Styles for image elements
// Mainly positioning and background styles
.image {
  position: relative;
  width: 300px;
  height: 100vh;
  background-repeat: no-repeat;
  background-position: center;

  // This will align all even images to the left
  // For getting centered positioned images, respect the viewport
  &:nth-child(2n) {
    justify-self: start;
  }

  // Set each `background-image` using a SCSS `for` loop
  @for $i from 1 through 10 {
    &:nth-child(#{$i}) {
      background-image: url('https://cdn.jsdelivr.net/gh/lmgonzalves/scroll-based-animation/img/image#{$i}.jpg');
    }
  }
}

// Adjusting layout for small screens
@media screen and (max-width: 760px) {
  .container {
    // 1 column grid
    grid-template-columns: 1fr;
    // Fix image centering
    justify-items: center;
  }

  // Fix image centering
  .image:nth-child(2n) {
    justify-self: center;
  }
}
View Compiled
/**
 * Regex tested and matched against the following userAgents:
 * iPhone
 *   Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X)
 *   AppleWebKit/602.1.50 (KHTML, like Gecko)
 *   CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1
 * iPad
 *   Mozilla/5.0 (iPad; CPU OS 9_0 like Mac OS X)
 *   AppleWebKit/600.1.4 (KHTML, like Gecko)
 *   CriOS/45.0.2454.89 Mobile/13A344 Safari/600.1.4 (000205)
 */
 
const iOSChromeDetected = /CriOS/.test(navigator.userAgent);

if (iOSChromeDetected) {
  const getHeight = function getComputedHeightFrom(element) {
    const computedHeightString = getComputedStyle(element).height;
    const elementHeight = Number(computedHeightString.replace('px', ''));
    return elementHeight;
  };

  const calculateVh = function calculateVhFrom(elementHeight) {
    const approximateVh = (elementHeight / initialViewportHeight) * 100;
    const elementVh = Math.round(approximateVh);
    return elementVh;
  };

  const setDataAttribute = function setDataAttributeUsing(elementVh, element) {
    const dataAttributeValue = `${elementVh}`;
    element.setAttribute('data-vh', dataAttributeValue);
  };

  const setHeight = function setHeightBasedOnVh(element) {
    const landscape = orientation;
    const vhRatio = Number(element.dataset.vh / 100);
    if (landscape) {
      element.style.height = `${vhRatio * landscapeHeight}px`;
    } else {
      element.style.height = `${vhRatio * portraitHeight}px`;
    }
  };

  const initialize = function initializeDataAttributeAndHeight(element) {
    const elementHeight = getHeight(element);
    const elementVh = calculateVh(elementHeight);
    setDataAttribute(elementVh, element);
    setHeight(element);
  };

  const initialViewportHeight = window.innerHeight;
  const elements = Array.from(document.getElementsByClassName('vh-fix'));
  const statusBarHeight = 20;
  const portraitHeight = screen.height - statusBarHeight;
  const landscapeHeight = screen.width - statusBarHeight;

  window.onload = function() {
    window.addEventListener('orientationchange', function() {
      elements.forEach(setHeight);
    });

    elements.forEach(initialize);
  };
}



// DEMO

(function() {

  // Easing function used for `translateX` animation
  // From: https://gist.github.com/gre/1650294
  function easeOutQuad (t) {
    return t * (2 - t)
  }

  // Returns a random number (integer) between `min` and `max`
  function random (min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min
  }

  // Returns a random number as well, but it could be negative also
  function randomPositiveOrNegative (min, max) {
    return random(min, max) * (Math.random() > 0.5 ? 1 : -1)
  }

  // Set CSS `tranform` property for an element
  function setTransform (el, transform) {
    el.style.transform = transform
    el.style.WebkitTransform = transform
  }

  // Current scroll position
  var current = 0
  // Target scroll position
  var target = 0
  // Ease or speed for moving from `current` to `target`
  var ease = 0.075
  // Utility variables for `requestAnimationFrame`
  var rafId = undefined
  var rafActive = false
  // Container element
  var container = document.querySelector('.container')
  // Array with `.image` elements
  var images = Array.prototype.slice.call(document.querySelectorAll('.image'))
  // Variables for storing dimmensions
  var windowWidth, containerHeight, imageHeight

  // Variables for specifying transform parameters and limits
  var rotateXMaxList = []
  var rotateYMaxList = []
  var translateXMax = -200

  // Popullating the `rotateXMaxList` and `rotateYMaxList` with random values
  images.forEach(function () {
    rotateXMaxList.push(randomPositiveOrNegative(20, 40))
    rotateYMaxList.push(randomPositiveOrNegative(20, 60))
  })

  // The `fakeScroll` is an element to make the page scrollable
  // Here we are creating it and appending it to the `body`
  var fakeScroll = document.createElement('div')
  fakeScroll.className = 'fake-scroll'
  document.body.appendChild(fakeScroll)
  // In the `setupAnimation` function (below) we will set the `height` properly

  // Geeting dimmensions and setting up all for animation
  function setupAnimation () {
    // Updating dimmensions
    windowWidth = window.innerWidth
    containerHeight = container.getBoundingClientRect().height
    imageHeight = containerHeight / (windowWidth > 760 ? images.length / 2 : images.length)
    // Set `height` for the fake scroll element
    fakeScroll.style.height = containerHeight + 'px'
    // Start the animation, if it is not running already
    startAnimation()
  }

  // Update scroll `target`, and start the animation if it is not running already
  function updateScroll () {
    target = window.scrollY || window.pageYOffset
    startAnimation()
  }

  // Start the animation, if it is not running already
  function startAnimation () {
    if (!rafActive) {
      rafActive = true
      rafId = requestAnimationFrame(updateAnimation)
    }
  }

  // Do calculations and apply CSS `transform`s accordingly
  function updateAnimation () {
    // Difference between `target` and `current` scroll position
    var diff = target - current
    // `delta` is the value for adding to the `current` scroll position
    // If `diff < 0.1`, make `delta = 0`, so the animation would not be endless
    var delta = Math.abs(diff) < 0.1 ? 0 : diff * ease

    if (delta) { // If `delta !== 0`
      // Update `current` scroll position
      current += delta
      // Round value for better performance
      current = parseFloat(current.toFixed(2))
      // Call `update` again, using `requestAnimationFrame`
      rafId = requestAnimationFrame(updateAnimation)
    } else { // If `delta === 0`
      // Update `current`, and finish the animation loop
      current = target
      rafActive = false
      cancelAnimationFrame(rafId)
    }

    // Update images
    updateAnimationImages()

    // Set the CSS `transform` corresponding to the custom scroll effect
    setTransform(container, 'translateY('+ -current +'px)')
  }

  // Calculate the CSS `transform` values for each `image`, given the `current` scroll position
  function updateAnimationImages () {
    // This value is the `ratio` between `current` scroll position and image's `height`
    var ratio = current / imageHeight
    // Some variables for using in the loop
    var intersectionRatioIndex, intersectionRatioValue, intersectionRatio
    var rotateX, rotateXMax, rotateY, rotateYMax, translateX

    // For each `image` element, make calculations and set CSS `transform` accordingly
    images.forEach(function (image, index) {
      // Calculating the `intersectionRatio`, similar to the value provided by
      // the IntersectionObserver API
      intersectionRatioIndex = windowWidth > 760 ? parseInt(index / 2) : index
      intersectionRatioValue = ratio - intersectionRatioIndex
      intersectionRatio = Math.max(0, 1 - Math.abs(intersectionRatioValue))
      // Calculate the `rotateX` value for the current `image`
      rotateXMax = rotateXMaxList[index]
      rotateX = rotateXMax - (rotateXMax * intersectionRatio)
      rotateX = rotateX.toFixed(2)
      // Calculate the `rotateY` value for the current `image`
      rotateYMax = rotateYMaxList[index]
      rotateY = rotateYMax - (rotateYMax * intersectionRatio)
      rotateY = rotateY.toFixed(2)
      // Calculate the `translateX` value for the current `image`
      if (windowWidth > 760) {
        translateX = translateXMax - (translateXMax * easeOutQuad(intersectionRatio))
        translateX = translateX.toFixed(2)
      } else {
        translateX = 0
      }
      // Invert `rotateX` and `rotateY` values in case the image is below the center of the viewport
      // Also update `translateX` value, to achieve an alternating effect
      if (intersectionRatioValue < 0) {
        rotateX = -rotateX
        rotateY = -rotateY
        translateX = index % 2 ? -translateX : 0
      } else {
        translateX = index % 2 ? 0 : translateX
      }
      // Set the CSS `transform`, using calculated values
      setTransform(image, 'perspective(500px) translateX('+ translateX +'px) rotateX('+ rotateX +'deg) rotateY('+ rotateY +'deg)')
    })
  }

  // Listen for `resize` event to recalculate dimmensions
  window.addEventListener('resize', setupAnimation)
  // Listen for `scroll` event to update `target` scroll position
  window.addEventListener('scroll', updateScroll)

  // Initial setup
  setupAnimation()

})()
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.