How to create a CSS-only infinite scroll animation

Written by Web Developer

October 15, 2023
How to create a CSS-only infinite scroll animation

In a previous article, we made an image slider with an infinite loop animation. The challenge was to use minimal HTML code, only CSS, and without any image duplication. Here is a demo of the slider

All you have to do is add as many images as you want and update one variable to match the number of images.

In this article, we will improve that slider to add more features. In the above demo, we see only one image at a time so why not show more? We will introduce another variable that controls the number of visible images. We will also see how to animate other elements and not only images.

The Code Structure

The HTML code is the same as the one in the previous article. A list of images within a container.

<div class="gallery">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">

For the CSS, we are going to rely on a grid to place the elements and to define the number of visible images. Here is an illustration to better understand.

Overview of the grid configuration

We have 5 images in total and only 3 are showing inside the container. Imagine the images sliding from left to right (or the opposite). The ones outside the container will be hidden.

The container is a grid of 3 columns and the CSS code will look like below:

$n:5; /* number of images*/

.gallery  {
  display: grid;
  grid-template-columns: repeat(3,200px); /* number of visible images + width */
  overflow: hidden;
.gallery > img {
  grid-area: 1/1;
  width: 100%;

You are probably wondering why I am using grid-area: 1/1 to place the images above each other. It’s because I am going to rely on the same animation technique used in the previous article so I highly recommend you read that article before this one. In this article, I will focus on adding new features while reusing the code of the old slider.

The Animation

Like the previous slider, we will define one animation for all the images then we will use a different animation-delay for each one.

$n:5; /* number of images*/

.gallery  {
  --d: 10s; /* duration */
.gallery > img {
  animation: r var(--d) linear infinite;
@for $i from 2 to ($n + 1) {
  .gallery > img:nth-child(#{$i}) {animation-delay: calc(#{(1 - $i)/$n}*var(--d))}

I used the code of the previous slider. The only difference will be the definition of the animation (the @keyframes).

Overview of the CSS animation

The @keyframes will have two parts:

  1. Initially, the image is placed on the first column. We slide it to the left until it’s completely hidden. We will use transform: translate(-100%)
  2. When the image is completely hidden, we instantly move it to the end. Meanwhile, the other images have moved as well. We will use transform: translate(($n - 1)*100%). From there it will move back to its initial position (transform :translate(0))

The code will be as follow:

@keyframes r {
  X% {transform: translate(-100%)}
  (X + .01)% {transform: translate(($n - 1)*100%)}

Now, we need to identify the X value. In the first part of the animation, the image is moving by one time its size while in the second part, it’s moving by (n-1) time its size. We get the following equation:

T1 + T2 = 100% 
T2 = (n - 1)*T1

Both parts together define the whole @keyframes hence their sum is equal to 100%. From this we get

T1 = 100%/n

Which is also the value of X

@keyframes r {
  #{100/$n}% {transform: translate(-100%)}
  #{100/$n + .01}% {transform: translate(($n - 1)*100%)}

Our new slider is done and its code is very similar to the previous one. We only updated the grid configuration to show more images and we adjusted the animation.

Adding The Gap

Since we are showing more images, controlling the gap between them would be a good idea. The only issue is that adding gaps will break our animation since this one relies only on the image sizes to perform the translation. Now, it will be different because we need to account for the gap when doing the translation.

To make things easier I will define the gap as a percentage of the image width.

$n:5; /* number of images*/
$w:200px; /* width */
$g:.2; /* gap as percentage of width */

.gallery  {
  display: grid;
  grid-template-columns: repeat(3,$w); /* number of visible images */
  gap: $g*$w;

The code should be self-explanatory but you are probably wondering why I am using Sass variables and not CSS variables. That’s because the percentage of the @keyframes will also depend on those variables and CSS variables cannot be used there.

For the animation, we need to update the second translation from transform: translate(($n - 1)*100%) to transform: translate(($n - 1 + ($n - 2)*$g)*100%). Between n - 1 images we have n - 2 gaps that we need to account for.

Same for the percentages. Our new equation will be equal to:

T1 + T2 = 100% 
T2 = (n - 1 + (n - 2)*g)*T1

And the new @keyframes will be

@keyframes r {
  #{100/($n + ($n - 2)*$g)}% {transform: translate(-100%)}
  #{100/($n + ($n - 2)*$g) + .01}% {transform: translate(($n - 1 + ($n - 2)*$g)*100%)}

When the gap is equal to 0 we get back to the previous slider which is nothing but a particular case of this one.

We are done! We have a nice infinite scroll animation where we can easily control the total number of images, the number of visible images, and the gap. Worth noting, that the number of visible images needs to be smaller than the total number of images. Since we are not duplicating the images we cannot see the same one twice.

What about a left-to-right animation? Will we need a new code?

No, the same code can be used to have the opposite direction. All you need to do is to add the keywords reverse inside the animation

More examples

Now that we have built our slider using images, let’s animate more elements. As long as you keep the same code structure you can create an infinite scroll animation with any elements. The only restriction is “equal width elements”.

Let’s animate some text content

In the above, I am not defining any explicit width. I am using 1fr to tell the browser to consider the width of the longest word. Don’t forget that all the elements are placed above each other in the first column so the width of that column needs to fit all the elements (in particular the longest one) and by using 1fr we make all the columns equal.

I am also not defining the gap like previously. Instead, I am using a transparent border. This is another way to introduce gaps while using the code of the slider without gaps. It may sound confusing but in reality, we don’t have gaps between the elements if we consider the grid structure but by making some part of the elements transparent (border in this case) we simulate gaps between elements.

You may think that using a transparent border is an easier way to add gaps. True, but in some cases, you may not be enable to do it and you have to rely on the other method. It’s always good to know different methods.

We animated images, we animated text and we can also animate complex components combining images and texts.


In the previous article, we built a simple slider of images that we enhanced in this article to consider more features. Now you can control the number of visible elements, the gap, and the direction of the animation. You can also animate any kind of content, the only restriction is to use equal width elements.

And we did this using a CSS-only solution. No need for any JavaScript and no need to duplicate elements.


Enter Discount Code MOVEME During The SIGN UP Process To Get 90% Off Your First Month

with the discount code


Use Code Now
Jivo Live Chat