Create an Infinite Horizontal Scroll Animation with Tailwind CSS
Tags: html, tailwindcss, webdev, tutorial
Published: 2023-08-29 13:19:05

####**[Live Demo](https://cruip-tutorials.vercel.app/logo-carousel/) / [Download](https://github.com/cruip/cruip-tutorials/blob/main/logo-carousel/index.html)**

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](https://cruip.com/stellar/), or our [recruitment website template](https://cruip.com/talent/).

Let’s begin!

Create the HTML structure
-------------------------

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

```html
<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.

```javascript
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.

```html
<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

```html
<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

```html
<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](https://github.com/cruip/cruip-tutorials-next/blob/main/components/logo-carousel.tsx) and [Vue](https://github.com/cruip/cruip-tutorials-vue/blob/main/src/components/LogoCarousel.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](https://cruip.com/tutorials/) or our gallery of [Tailwind CSS templates](https://cruip.com/) if you need a starting point for your next project.