Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

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

              
            
!

CSS

              
                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;
  }
}

              
            
!

JS

              
                /**
 * 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()

})()

              
            
!
999px

Console