How to create scroll animations: a comprehensive pocket guide

calendar icon
29 Feb
16 Aug

Whether you are a seasoned web developer or just starting out on this IT journey, scroll animations are a valuable tool to have in your skill set. They can add a touch of life and interactivity to the website you create, which will be a great bonus in the eyes of your client and their audience. And today we’d like to share with you some insights on their implementation.

This article, along with its twin sister, is a good place to begin or continue mastering the art of animations. To inspire and guide you, we will showcase a variety of examples of our work, with step-by-step explanations of the code behind them.

While some technical knowledge may be helpful to fully understand this guide, we welcome readers of all backgrounds to explore the information our team has prepared below. So, regardless of your coding experience, we hope to share valuable insights with you. Who knows, perhaps this article will motivate you to start your own journey in web development.

Let’s nail the basics first

When you put an element on a webpage, it’s not exactly a big deal, right? But when you add an animation effect that brings this element to life, as if an invisible hand is drawing it, many perceive it as some sort of digital magic.

Whatever elements you want to animate — artwork, illustration, or shape — you can always represent it using SVG, just like in this example:

To make it possible, we need just a slice of JavaScript because most work is done in CSS. In the JS code, we need to obtain the path element for the SVG image, determine the path length, and pass the resulting value to the :root CSS element.

const path = document.querySelector(’.path’);
const length = path.getTotalLength();’--length’, length);

The :root element in the CSS code contains the --length variable, which stores the path length. Since the initial value of --length is unknown, we take it to be 0. When the page loads, JavaScript will pass the real value of --length to :root.

:root {
  --length: 0;}

In the code below, the stroke-dasharray property is used to create an SVG contour and is equal to the path length. The stroke-dashoffset property shifts the SVG contour by the path length. The combination of these two properties results in SVG not being drawn.

.path {
  stroke-dasharray: var(--length);
  stroke-dashoffset: var(--length);
  stroke-width: 2;
  animation: anim 3s ease-in-out alternate infinite;}

The anim element is responsible for drawing the SVG image. The 3s specified in the animation properties determine the duration of the animation, ease-in-out is a function that provides the animation’s smooth playback, the alternate value indicates that the animation changes direction in each cycle, and the infinite value corresponds to an infinite animation.

That’s how the animation code looks like:

@keyframes anim {
  0% {
    stroke-dashoffset: var(--length);}
  100% {
    stroke-dashoffset: 0;}

In this code snippet, we change the value of the stroke-dashoffset animation contour shift from the path length when SVG is not drawn at all to zero and when SVG is drawn completely.

One-screen scroll animation

The one-screen scroll animation is an interactive effect that allows you to animate an SVG drawing based on mouse wheel scrolling. This animation occurs within a single screen or viewport, without affecting the scroll behavior of the entire page. Here’s how this looks in the code:

Since we need to track the page scrolling, we should transfer the animation from CSS to JavaScript. Then, we need to change the value of the CSS stroke-dashoffset property when scrolling the mouse wheel. When the value of stroke-dashoffset decreases, the image is drawn; when the value of stroke-dashoffset increases, the image is erased.

The event.deltaY variable allows us to track the scrolling of the mouse wheel.

window.addEventListener(’wheel’, event => console.log(event.deltaY));

Event.deltaY values greater than zero correspond to the page scrolling down.

Event.deltaY values less than zero correspond to the page scrolling up.

In the JavaScript code, we have to create the wheel variable that will be used to draw the SVG and contain the number of pixels scrolled vertically. The initial value of the wheel variable before scrolling starts is 0:

let wheel = 0;

Let’s create the drawSvg function, which will be executed when the mouse wheel is scrolled.

function drawSvg(event) {
  let deltaY = event.deltaY;
  let wheelLength = wheel + deltaY;
  let drawLength = length - wheelLength;

  if (drawLength >= 0 && drawLength <= length) {
    wheel = wheelLength; = drawLength;
  } else if (drawLength < 0) {
    wheelLength = 0; = 0;
  } else if (drawLength > length) {
    wheelLength = length; = length;}

In this function, the deltaY variable contains the value of each mouse wheel scroll, the wheelLength variable contains the total scroll length, and the drawLength variable contains the length of the drawn part of the image’s path. Now, let’s transfer the resulting drawLength value to CSS styles: = drawLength;

The if...else conditions in the drawSvg function indicate that the path element of the SVG image is drawn or erased until the image is completely drawn or completely erased from the page. After that, image drawing and changing the value of the wheel variable stops until the direction of scrolling of the mouse wheel changes.

The drawSvg function is executed when the mouse wheel is scrolled, and for this, we track the wheel event:

window.addEventListener(’wheel’, drawSvg);

Scroll-bound animation

Scroll-bound animations are great for creating parallax effects. This enables different elements to link an element’s position to the page’s height, scroll at different speeds, create a 3D effect, and bring a touch of depth and life to your project.

Now let’s take a look at the next example — here, the SVG image looks almost the same as the previous one, but the code responsible for drawing this animation is completely different:

Pay attention to the change in CSS styles — we specify the page height equal to 200vh, whereas for the previous animation, the page height was 100vh. This is necessary in order to ensure that the page scrolls in the browser window, as this animation will not be played without scrolling the page.

In the drawSvg function, we need to initialize the following variables:

  • bodyScrollTop: the number of pixels scrolled from the top of the page;
  • elementScrollTop: the number of pixels scrolled from the top of the element (distance from the element’s top to its topmost visible content);
  • elementScrollHeight: the height of the element, including the content invisible due to scrolling (height of an element’s content, including content that is not visible on the screen due to overflow);
  • elementClientHeight: the height of the element (inner height of an element in pixels).
let bodyScrollTop = document.body.scrollTop;
let elementScrollTop = document.documentElement.scrollTop;
let elementScrollHeight = document.documentElement.scrollHeight;
let elementClientHeight = document.documentElement.clientHeight;

Now, we have to use the obtained values to calculate the percentage by which the element was scrolled:

let scrollpercent = (bodyScrollTop + elementScrollTop) /           (elementScrollHeight - elementClientHeight);

The next step is to calculate the length of the drawn path part of the SVG image and pass it to the stroke-dashoffset CSS property:

let draw = length * scrollpercent; = length - draw;

The drawSvg function is executed when the page is scrolled, for this we need to track the scroll event:

window.addEventListener(’scroll’, drawSvg);

Scroll-triggered animation

Scroll-triggered SVG drawing animation refers to a dynamic visual effect that is activated when the user scrolls to a specific section of a webpage. It involves using SVG to create and animate graphical elements such as lines, shapes, and paths.

To better illustrate how this animation works, we added two additional sections to the project, which are placed above and below the section with the animation:

The smooth scrolling of the sections is achieved by implementing several CSS properties, including overflow-y with the scroll value,scroll-snap-type value for the container and scroll-snap-align for the sections — and don’t forget to specify the height of the container and sections. Another useful property of the html element is scroll-behavior with the smooth value, which allows for smoother scrolling.

CSS properties mentioned above are not directly relevant to the drawing of an SVG image during scrolling. They, however, enable a smoother and more user-friendly scrolling experience by dividing the page into sections, which positively impacts the user experience.

To incorporate the SVG image into the CSS code of the project, we will append the draw class that animates the SVG drawing from the basic example.

.draw {
  animation: anim 3s ease-in-out forwards;}

Adding the draw class to the path element will display the SVG contour; removing the draw class will respectively hide this contour. The key to adding and removing the draw class to the path element at the appropriate times is to constantly monitor the section’s visibility zone. When the section with the image becomes visible, we need the draw class added to the path element. Conversely, when the section is no longer visible, we need to remove the draw class.

The Intersection Observer API allows you to notify the application that an element has appeared in the visibility zone. One major advantage of this approach is that it reduces the computational load on the browser by minimizing the need for additional calculations as compared to monitoring the scroll event.

The Intersection Observer API provides numerous useful capabilities, including deferred image loading, infinite page scrolling, ad visibility tracking, and the ability to activate modifications such as animations when the elements enter the user’s viewport.

Here’s the code we used for Intersection Observer in our example:

let options = {
  threshold: [0.5, 1],};
function drawSvg(items) {
  if (items[0].isIntersecting) {
  } else {
const iObserver = new IntersectionObserver(drawSvg, options);

In our Intersection Observer code, the options parameters indicate that the observer will be activated when the monitored element is at least half visible on the screen and remains within the viewport.

The drawSvg function is executed when the observer is triggered, specifically when an element appears or disappears from the viewport. In the penultimate line of code, the IntersectionObserver constructor is accessed, and both the options and drawSvg function are passed to it. Finally, in the last line, the IntersectionObserver is instructed to monitor the designated element.

Let’s take things up a notch

Now that we’ve covered the general principles of creating scroll animations, how about we dig deeper into more complex and challenging cases?

One-screen scroll animation — vertical skew slider

You already know how to handle basic animations’ code, so we invite you to take it to the next level. Let’s discuss how to execute vertical skew sliders, parallax, and scroll-triggered animations.

The first challenging example in our to-learn list is a vertical skew slider that activates on scroll.

Each slide consists of two parts — text and image. When scrolling the slider, these parts move in different directions at an angle relative to each other.

The vertical skew slider brings content to life as it activates on scroll, captivating users with its smooth movements.

Take a glance at how great this interactive element looks and how easily you can create an animation triggered with the mouse wheel scroll:

In the first lines of code, we declare constants:

  • sliderContainer, which corresponds to the DOM element with the slider-container class;
  • slides, which contains all slides with the slide class;
  • slidesLength, which we assign a value equal to the number of slides.
const sliderContainer = document.querySelector('.slider-container');
const slides = document.querySelectorAll('.slide');
const slidesLength = slides.length;

We declare the activeSlideIndex variable and assign it a value of 0. This means that the first slide will be displayed by default.

let activeSlideIndex = 0;

The moveToSlide function changes the active slide number depending on the value passed in the swap parameter. This parameter can take one of two values: +1 when scrolling the mouse wheel down, or -1 when scrolling the mouse wheel up.

Now we determine the index of the currently active slide by finding the remainder of a division operation. This approach ensures that the slide movement loops back to the beginning after reaching the end of the sequence, creating a cyclic effect. Also, we should add scrolling active class to the active slide.

const moveToSlide = (swap) => {
  if (sliderContainer.classList.contains('slider-locked')) return;

  activeSlideIndex = (activeSlideIndex + swap + slidesLength) % slidesLength;


  slides.forEach((slide, index) => {
    slide.classList.toggle('scrolling_active', index === activeSlideIndex);});}

While the active slide is being moved, we apply the slider-locked class to the sliderContainer element. If sliderContainer already has the slider-locked class, the moveToSlide function will not execute and will instead return. This prevents the slides from being scrolled while the active slide is being moved.

The onTransitionEnd function removes the slider-locked class from sliderContainer when the animation is complete.

const onTransitionEnd = () => {

The function onScroll is called when the mouse wheel is scrolled. Here, it is highly important to avoid triggering the default action. At first, using the Math.sign() method, we normalize the value of deltaY to read only the direction of scrolling, and pass it as an argument. Then, we should update the active slide through the moveToSlide function.

const onScroll = (event) => {
  const swap = Math.sign(event.deltaY);

Now we have to add the wheel event handler to the document to track mouse wheel scrolling and call the onScroll function. The transitionend event handler is included in the sliderContainer to call the onTransitionEnd function when the animation is finished.

document.addEventListener('wheel', onScroll);
sliderContainer.addEventListener('transitionend', onTransitionEnd);

As a result, we will get the endlessly repeating slider that seamlessly transitions between slides as the user scrolls the mouse wheel.

Scroll-bound animation — parallax

Scroll-based animations are great for creating parallax effects.

Linking an element’s position to the page’s scroll height provides precise control over its properties based on the scroll height itself. In addition, it allows for scrolling various elements at different speeds, creating a 3D effect that can add dimension to any web page.

With scroll-bound animations, the scrolling experience becomes an immersive adventure, as visuals respond to your every move, creating a sense of dynamism.

To give you a better coding picture, here’s a CodePen example of the parallax effect:

It’s worth noting that we use a tiny bit of JavaScript code for our parallax effect, with most of the heavy lifting done by CSS. While it’s possible to create a parallax effect using CSS alone, JavaScript provides additional possibilities that pure CSS does not. For instance, with JavaScript, we can more precisely control the speed at which different layers move as we scroll, relocate elements in different directions and angles, resize them, or use other transformations.

Let’s take a look at the JavaScript code implemented in our example. Here, we use the GSAP library with ScrollTrigger and ScrollSmoother plugins for smoother scrolling. You can create a parallax effect without using this library, making it optional rather than necessary. However, using smooth scrolling provides better animation playback, which is essential for achieving an excellent parallax effect. By doing so, you can enhance user interaction with the page and improve its overall usability.

gsap.registerPlugin(ScrollTrigger, ScrollSmoother)
	wrapper: '.wrapper',
	content: '.content'});

The handleScroll function receives the root element :root from CSS and assigns the value of the page scroll height to the scrollTop variable.

const handleScroll = () => {
  const scrollTop = document.documentElement.scrollTop;'--scrollTop', `${scrollTop}px`);}

This function is called during page scrolling, which updates the value of the scrollTop variable.

document.addEventListener('scroll', () => {

To optimize performance when calling the scrollTop function during scrolling, we can use the requestAnimationFrame method, which allows a specific function to be executed only when a browser is ready to draw the next frame. This approach can help reduce CPU load, improve browser performance, and increase scrolling smoothness.

Now, let’s check the value of the scrollTop variable used in the CSS code to shift parallax layers vertically.

.layer-base {
  background-image: url('');
transform: translateY(calc(var(--scrollTop) / 1.6));}

.layer-middle {
  background-image: url('');
  transform: translateY(calc(var(--scrollTop) / 2.5));}

.layer-front {
  background-image: url('');
  transform: translateY(calc(var(--scrollTop) / 5.7));}

Different parallax layers use different coefficients, so their displacement speeds will not be the same. The coefficients are arbitrary, but the general principle when selecting them is that more distant layers and elements move slower, while the ones closer to us move faster, creating an impression of a voluminous space.

Scroll-triggered animation — image effects on scroll

Image effects on scroll is a type of scroll-triggered animation where images on a webpage change or transform as the user scrolls past them. This can be achieved using a variety of techniques, such as changing the image color, size, opacity, or position, adding special effects like zooming, rotating, blurring, or transitioning between different images.

Scroll-triggered animations bring a new level of interactivity to the digital landscape, where static images come alive, revealing every tiny hidden detail.

By using different tools, such as Intersection Observer, we can always determine when an element or section appears within the user’s viewport and attach any animation to that moment. Doing so will help us catch the user’s eye, making the web page more engaging and memorable to use.

In the example below, two animations are triggered when the section with the image appears on the screen: the black & white image transforms and becomes colorful.

To give you a better picture, here’s an example with parallax effect:

In this case, we should first find the target element on the page — all sections with the section class:

const targets = document.querySelectorAll(".section");

After, we need to indicate that the animation will start playing when the section is scrolled on the screen at least halfway:

const threshold = 0.5;

Furthermore, you should note that Intersection Observer always considers the direction of scrolling. This does not affect threshold = 0.5; though, as half remains half regardless of the scrolling direction. But if the animation should start when the section has just appeared on the screen, we need to determine in which direction the user is scrolling the page. Fortunately, Intersection Observer can take care of these calculations.

The setAnim function takes the entries array of objects, each representing information about the element that intersects the viewport boundary, and the observer object.

The forEach() method is used inside the setAnim function to iterate through all the objects in the entries array. For each entry object in the array, the target method is called, which returns the DOM element associated with the entry objects.

const setAnim = (entries, observer) => {
  entries.forEach((entry) => {
    const elem =;
    if (entry.isIntersecting) {
    } else {
      elem.classList.remove('anim'); }

Then, we’ll create a new instance of IntersectionObserver, passing the setAnim function and the threshold value to it, and observe each section in the targets array that we passed.

const observer = new IntersectionObserver(setAnim, { threshold });
for (const target of targets) {

This way, IntersectionObserver tracks the intersection of elements with the visible area (viewport) on the page.

The for...of loop is used to call the observe() method for each target element in the targets array. This registers each element for observation and will trigger the setAnim function when the element intersects the visibility threshold (intersection).

Then, if the value of the isIntersecting property of the entry object is true, the function adds the anim class to the element using the classList.add() method. If the value of the isIntersecting property is false, the function removes the anim class from the element using the classList.remove() method.

We can add various animations to sections with the anim class and any nested elements. Also, whenever possible, it’s better to add animations using CSS as it optimizes their playback and improves code execution performance.

To animate the transformation of an image from black and white to color, we will use CSS filters. And to gradually add color to the image, we need to add an absolutely positioned pseudo-element ::after to the block with the colored image. It has the exact same size and image but with the filter: grayscale(1); property applied to it, which makes the image appear in black and white. During the animation, we change the height of the pseudo-element from 100% to 0, revealing the original colored image behind it.

To transform elements, we use properties such as perspective, rotate, translate3d, skew, scale, and others.

Final line of our coding guide

Animations are everywhere nowadays and there’s a good reason for that. Many users and site owners appreciate them, as these visual effects can improve usability, increase conversion rates, and create a “wow” effect.

But implementation of any animation type, be it scroll-triggered, or a vertical skew slider, requires lots of experience and a sharp eye for detail. So we advise you to constantly improve your skills, gaining knowledge and inspiration from different sources, like dev-related articles or projects on Awwwards, Dribbble, Behance, and Webflow.

And, of course, don’t forget to share this text with your developer friends! See you in the next article — have a nice day and a clean code :)

Writing team:
Technical writer
Have a project
in your mind?
Let’s communicate.
Get expert estimation
Get expert estimation
expert postexpert photo

Frequently Asked Questions

No items found.

Frequently Asked Questions

copy iconcopy icon

Ready to discuss
your project with us?

Please, enter a valid email
By sending this form I confirm that I have read and accept the Privacy Policy

Thank you!
We will contact you ASAP!

error imageclose icon
Hmm...something went wrong. Please try again 🙏
error icon
clutch icon

Our clients say

The site developed by Halo Lab projected a very premium experience, successfully delivering the client’s messaging to customers. Despite external challenges, the team’s performance was exceptional.
Aaron Nwabuoku avatar
Aaron Nwabuoku
Founder, ChatKitty
Thanks to Halo Lab's work, the client scored 95 points on the PageSpeed insights test and increased their CR by 7.5%. They frequently communicated via Slack and Google Meet, ensuring an effective workflow.
Viktor Rovkach avatar
Viktor Rovkach
Brand Manager at felyx
The client is thrilled with the new site and excited to deploy it soon. Halo Lab manages tasks well and communicates regularly to ensure both sides are always on the same page and all of the client’s needs are addressed promptly.
Rahil Sachak Patwa avatar
Rahil Sachak Patwa
Founder, TutorChase

Join Halo Lab’s newsletter!

Get weekly updates on the newest stories, posts and case studies right in your mailbox.

Thank you for subscribing!
Please, enter a valid email
Thank you for subscribing!
Hmm...something went wrong.