Scroll Based Animation in CSS

Scroll Based Animation in CSS
Code Snippet:Scroll Based Animation
Author: lmgonzalves
Published: January 21, 2024
Last Updated: January 22, 2024
Downloads: 807
License: MIT
Edit Code online: View on CodePen
Read More

This HTML CSS & JavaScript code snippet implements a Scroll Based Animation on a webapge. It creates a custom scroll effect for a set of images within a container. The images move and rotate based on your scroll position, creating a visually engaging scrolling experience.

You can use this code on your website to enhance user engagement and make your content more interactive. It adds a dynamic scrolling animation to images, making your site visually appealing.

How to Create Scroll Based Animation In CSS

1. First of all, load the Normalize CSS by adding the following CDN link into the head tag of your HTML document.

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">

2. After that, create the HTML structure for your scroll-based animation. Place the images you want to animate within a container. Each image should be wrapped in a <div> with the class “image.”

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

3. Next, apply CSS styles to achieve the desired animation effect. The provided CSS code contains the necessary styles for this effect. It sets up the grid layout, image positioning, and animation properties.

body{
  overflow-x: hidden;
  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;
}

.fake-scroll {
  position: absolute;
  top: 0;
  width: 1px;
}

.container {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-gap: 0 10%;
  justify-items: end;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
}

.image {
  position: relative;
  width: 300px;
  height: 100vh;
  background-repeat: no-repeat;
  background-position: center;
}
.image:nth-child(2n) {
  justify-self: start;
}
.image:nth-child(1) {
  background-image: url("https://cdn.jsdelivr.net/gh/lmgonzalves/scroll-based-animation/img/image1.jpg");
}
.image:nth-child(2) {
  background-image: url("https://cdn.jsdelivr.net/gh/lmgonzalves/scroll-based-animation/img/image2.jpg");
}
.image:nth-child(3) {
  background-image: url("https://cdn.jsdelivr.net/gh/lmgonzalves/scroll-based-animation/img/image3.jpg");
}
.image:nth-child(4) {
  background-image: url("https://cdn.jsdelivr.net/gh/lmgonzalves/scroll-based-animation/img/image4.jpg");
}
.image:nth-child(5) {
  background-image: url("https://cdn.jsdelivr.net/gh/lmgonzalves/scroll-based-animation/img/image5.jpg");
}
.image:nth-child(6) {
  background-image: url("https://cdn.jsdelivr.net/gh/lmgonzalves/scroll-based-animation/img/image6.jpg");
}
.image:nth-child(7) {
  background-image: url("https://cdn.jsdelivr.net/gh/lmgonzalves/scroll-based-animation/img/image7.jpg");
}
.image:nth-child(8) {
  background-image: url("https://cdn.jsdelivr.net/gh/lmgonzalves/scroll-based-animation/img/image8.jpg");
}
.image:nth-child(9) {
  background-image: url("https://cdn.jsdelivr.net/gh/lmgonzalves/scroll-based-animation/img/image9.jpg");
}
.image:nth-child(10) {
  background-image: url("https://cdn.jsdelivr.net/gh/lmgonzalves/scroll-based-animation/img/image10.jpg");
}

@media screen and (max-width: 760px) {
  .container {
    grid-template-columns: 1fr;
    justify-items: center;
  }

  .image:nth-child(2n) {
    justify-self: center;
  }
}

4. The following JavaScript code is responsible for creating the custom scroll effect. It adjusts the image positions and rotations based on the scroll position. You can use this code as is, but make sure to place it within a <script> tag at the end of your HTML document, just before the closing </body> tag.

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

})()

That’s all! hopefully, you have successfully created a Scroll Based animation in HTML, CSS, and JavaScript. If you have any questions or suggestions, feel free to comment below.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

About CodeHim

Free Web Design Code & Scripts - CodeHim is one of the BEST developer websites that provide web designers and developers with a simple way to preview and download a variety of free code & scripts. All codes published on CodeHim are open source, distributed under OSD-compliant license which grants all the rights to use, study, change and share the software in modified and unmodified form. Before publishing, we test and review each code snippet to avoid errors, but we cannot warrant the full correctness of all content. All trademarks, trade names, logos, and icons are the property of their respective owners... find out more...

Please Rel0ad/PressF5 this page if you can't click the download/preview link

X