· Updated on

Create an Infinite Horizontal Scroll Animation with Tailwind CSS

Preview of the logo carousel we're going to build

In this tutorial, we will show you how to create a cool infinite horizontal animation using only CSS. This effect can be used to display a variety of things, and for this post, we’ll focus on building a logo carousel that smoothly scrolls from right to left using Tailwind CSS classes.

The best part is that you can simply copy and paste the code into your HTML page, and there’s no need for any custom CSS.
As the last thing before we begin, if you’re interested in seeing infinite horizontal scroll in action in a real-case scenario, look at our dark Next.js landing page template, or our recruitment website template.

Let’s begin!

Create the HTML structure

First, let’s set up a straightforward structure for our logo carousel using an unordered list.

<div class="w-full inline-flex flex-nowrap">
    <ul class="flex items-center justify-center md:justify-start [&_li]:mx-8 [&_img]:max-w-none">
        <li>
            <img src="./facebook.svg" alt="Facebook" />
        </li>
        <li>
            <img src="./disney.svg" alt="Disney" />
        </li>
        <li>
            <img src="./airbnb.svg" alt="Airbnb" />
        </li>
        <li>
            <img src="./apple.svg" alt="Apple" />
        </li>
        <li>
            <img src="./spark.svg" alt="Spark" />
        </li>
        <li>
            <img src="./samsung.svg" alt="Samsung" />
        </li>
        <li>
            <img src="./quora.svg" alt="Quora" />
        </li>
        <li>
            <img src="./sass.svg" alt="Sass" />
        </li>
    </ul>                
</div>

Each list item will represent a company’s logo, and we’ve added the alt attribute to every image to ensure accessibility.

The ul element has the classes flex and items-center, which make the logos stand in a row and be vertically centered.

Additionally, we’ve used some arbitrary Tailwind CSS variants to optimize the layout:

  • [&_li]:mx-8 adds a horizontal margin of 32px to each list item
  • [&_img]:max-w-none removes the default 100% maximum width applied to images, preserving their original size, even on smaller screens

Define the animation

Defining the animation is straightforward. All we need to do is translate the ul element to the left from 0 to -100%. However, Tailwind CSS doesn’t provide this specific animation out-of-the-box, so we’ll define it ourselves in our tailwind.config object.

Typically, you’d have a tailwind.config.js file in your project’s root directory. But since we’re using the Tailwind CDN, we’ll define the tailwind.config object within a script tag in our HTML file.

tailwind.config = {
  theme: {
    extend: {
      animation: {
        'infinite-scroll': 'infinite-scroll 25s linear infinite',
      },
      keyframes: {
        'infinite-scroll': {
          from: { transform: 'translateX(0)' },
          to: { transform: 'translateX(-100%)' },
        }
      }                    
    },
  },
}

We’ve named the animation infinite-scroll, and it’s a linear, infinite, and lasts for 25 seconds. We’ve also specified the keyframes of the animation, to translate from 0 to -100%.

Using this animation is as easy as adding the class animate-infinite-scroll to the element you wish to translate. In our case, that’s the ul element.

Make the animation loop

Now, if you preview the page, you’ll see that the animation works like a charm! But there’s one tiny hiccup – when the ul element reaches -100%, it suddenly jumps back to 0, breaking the seamless flow.

To avoid that, we’ll duplicate the ul element and position it right after the existing one. This way, when the animation reaches -100%, the duplicate element takes over, ensuring the animation continues without any disruptions.

We know redundant elements in HTML are a no-no, so for now, let’s add the aria-hidden="true" attribute to the duplicated element, making it invisible to screen readers. Later, we’ll use some just a bit of JavaScript to create the duplicate element dynamically.

<div class="w-full inline-flex flex-nowrap">
    <ul class="flex items-center justify-center md:justify-start [&_li]:mx-8 [&_img]:max-w-none animate-infinite-scroll">
        <li>
            <img src="./facebook.svg" alt="Facebook" />
        </li>
        <li>
            <img src="./disney.svg" alt="Disney" />
        </li>
        <li>
            <img src="./airbnb.svg" alt="Airbnb" />
        </li>
        <li>
            <img src="./apple.svg" alt="Apple" />
        </li>
        <li>
            <img src="./spark.svg" alt="Spark" />
        </li>
        <li>
            <img src="./samsung.svg" alt="Samsung" />
        </li>
        <li>
            <img src="./quora.svg" alt="Quora" />
        </li>
        <li>
            <img src="./sass.svg" alt="Sass" />
        </li>
    </ul>
    <ul class="flex items-center justify-center md:justify-start [&_li]:mx-8 [&_img]:max-w-none animate-infinite-scroll" aria-hidden="true">
        <li>
            <img src="./facebook.svg" alt="Facebook" />
        </li>
        <li>
            <img src="./disney.svg" alt="Disney" />
        </li>
        <li>
            <img src="./airbnb.svg" alt="Airbnb" />
        </li>
        <li>
            <img src="./apple.svg" alt="Apple" />
        </li>
        <li>
            <img src="./spark.svg" alt="Spark" />
        </li>
        <li>
            <img src="./samsung.svg" alt="Samsung" />
        </li>
        <li>
            <img src="./quora.svg" alt="Quora" />
        </li>
        <li>
            <img src="./sass.svg" alt="Sass" />
        </li>
    </ul>
</div>

Voila! The animation is now seamless and never-ending! If you find the speed too slow or too fast for your taste, go ahead and tweak the animation duration in the tailwind.config.js file.

Creating a gradient mask

Even though we’ve set a maximum width for our container, the animation goes a bit wild and extends beyond the edges, taking over the whole page. That’s totally fine, but if you want to keep things within the container, we definitely need a gradient mask.

Just add two classes to the element containing the unordered lists:

  • overflow-hidden, which hides anything that extends beyond the container’s borders
  • [mask-image:_linear-gradient(to_right,transparent_0,_black_128px,_black_calc(100%-128px),transparent_100%)], which defines a gradient mask at the container’s edges, transitioning from transparent to black
<div class="w-full inline-flex flex-nowrap overflow-hidden [mask-image:_linear-gradient(to_right,transparent_0,_black_128px,_black_calc(100%-200px),transparent_100%)]">
    <ul class="flex items-center justify-center md:justify-start [&_li]:mx-8 [&_img]:max-w-none animate-infinite-scroll">
        <!-- ... -->
    </ul>
    <ul class="flex items-center justify-center md:justify-start [&_li]:mx-8 [&_img]:max-w-none animate-infinite-scroll" aria-hidden="true">
        <!-- ... -->
    </ul>
</div>

Use a bit of JavaScript to clone the list

Alright, as mentioned earlier, now we’ll see how we can easily create the duplicate element using Alpine.js to make the code cleaner and more readable.

Here’s what we’re going to do:

  • Remove the duplicated list from the HTML
  • Define a x-data directive with an empty object
  • Add an x-ref attribute to the element containing the list to be duplicated, so we can refer to it in Alpine.js
  • Define the x-init directive with a function that works its magic when the page loads
<div
    x-data="{}"
    x-init="$nextTick(() => {
        let ul = $refs.logos;
        ul.insertAdjacentHTML('afterend', ul.outerHTML);
        ul.nextSibling.setAttribute('aria-hidden', 'true');
    })"
    class="w-full inline-flex flex-nowrap overflow-hidden [mask-image:_linear-gradient(to_right,transparent_0,_black_128px,_black_calc(100%-128px),transparent_100%)]"
>
    <ul x-ref="logos" class="flex items-center justify-center md:justify-start [&_li]:mx-8 [&_img]:max-w-none animate-infinite-scroll">
        <li>
            <img src="./facebook.svg" alt="Facebook" />
        </li>
        <li>
            <img src="./disney.svg" alt="Disney" />
        </li>
        <li>
            <img src="./airbnb.svg" alt="Airbnb" />
        </li>
        <li>
            <img src="./apple.svg" alt="Apple" />
        </li>
        <li>
            <img src="./spark.svg" alt="Spark" />
        </li>
        <li>
            <img src="./samsung.svg" alt="Samsung" />
        </li>
        <li>
            <img src="./quora.svg" alt="Quora" />
        </li>
        <li>
            <img src="./sass.svg" alt="Sass" />
        </li>
    </ul>                
</div>

The function defined in x-init simply copies the ul element and inserts it right after, adding the aria-hidden attribute to the duplicated element.

Conclusions

As for all our tutorials, we also included components for both Next.js and Vue, so you can easily integrate this animation in your favorite framework.

If you’re hungry for more guides and tips we invite you to look at our section of Tailwind CSS tutorials or our gallery of Tailwind CSS templates if you need a starting point for your next project.