DEV Community

Cover image for A Tailwind never-ending carousel (ticker)
Patrick O'Neill
Patrick O'Neill

Posted on

A Tailwind never-ending carousel (ticker)

I recently wanted a never-ending carousel (I'm not sure of the correct terminology and ticker maybe more correct).

I'm making a side project Wedding Photo Collector and I wanted this for my hero section.

So here is a quick tutorial mostly inspired by this video.

I'm writing this in React but it should be easily convertible to other frameworks or just plain html.

First I start by defining the input Props of the component, which in this case are an array of images which have a url and an alt tag.

type TickerProps = {
    images: { url: string; alt: string }[];
};
Enter fullscreen mode Exit fullscreen mode

Now the next thing was to create the React component and create a <ul> filled with the images.

type TickerProps = {
    images: { url: string; alt: string }[];
};

const Ticker = ({ images }: TickerProps) => {

    return (
        <ul className="">
            {images.map((image) => (
                <li key={image.url} className="">
                    <img
                        src={image.url}
                        alt={image.alt}
                        className=""
                    />
                </li>
            ))}
        </ul>
    );
};

export default Ticker;
Enter fullscreen mode Exit fullscreen mode

Next thing to do is create the animation and to do this we need to edit the tailwind.config.ts we need to add the code below.

keyframes: {
    'image-scroll': {
        '0%': { transform: 'translateX(0)' },
        '100%': { transform: 'translateX(-100%)' },
    },
},
animation: {
    'image-scroll': 'image-scroll 20s linear infinite',
},
Enter fullscreen mode Exit fullscreen mode

You can tweak the 20s value so that the speed makes sense with the amount of images or content you have.

We will now add a few tailwind classes to the ul. animate-image-scroll to apply the animation we just made above. We also want to pop it in a flex container with shrink-0 so the container won't shrink to fit the available size. We also want to add gap-8 and pr-8 to add spacing between the images, tweak the 8 to whatever value looks good for you.

For the <li> we want to add shrink-0 and on the <img> we add h-full to make the images fill the container height, object-cover to make the image fill the container.

At this stage I found a discrepancy in the way Chrome, Firefox and Safari treated this and the fix was to apply a fixed width (otherwise in Firefox and Safari the width would be set by the width of the image file even though the image is scaled down with object-cover) so I add w-[180px].

This should give us...

type TickerProps = {
    images: { url: string; alt: string }[];
};

const Ticker = ({ images }: TickerProps) => {

    return (
        <ul className="animate-image-scroll flex shrink-0 gap-8 pr-8">
            {images.map((image) => (
                <li key={image.url} className="shrink-0">
                    <img
                        src={image.url}
                        alt={image.alt}
                        className="h-full w-[180px] object-cover opacity-85"
                    />
                </li>
            ))}
        </ul>
    );
};

export default Ticker;
Enter fullscreen mode Exit fullscreen mode

If you run this you will notice that it's not never ending it will simply slide off the page.

So here is where the trick comes in. We actually have two <ul> and when the animation finishes the second <ul> will be in the starting position and when the animation starts again it will seamlessly switch out!

There is a small gotcha that we want to put aria-hidden="true" on the second ul for accessibility and so that it doesn't look like duplicate content.

To do this I created an array and mapped over it.

type TickerProps = {
    images: { url: string; alt: string }[];
};

const Ticker = ({ images }: TickerProps) => {
    const ariaHidden = [false, true];

    return (
        <div className="flex h-64">
            {ariaHidden.map((hidden, idx) => (
                <ul className="animate-image-scroll flex shrink-0 gap-8 pr-8" aria-hidden={hidden} key={idx}>
                    {images.map((image) => (
                        <li key={image.url + idx} className="shrink-0">
                            <img
                                src={image.url}
                                alt={image.alt}
                                className="h-full w-[180px] object-cover opacity-85"
                            />
                        </li>
                    ))}
                </ul>
            ))}
        </div>
    );
};

export default Ticker;
Enter fullscreen mode Exit fullscreen mode

Now we are getting there and it should be fully functional there is just one more thing I wanted to add.

As my page is not full width I wanted a fade out at either side so that the images do not get cut off abruptly.

This could be done using absolute positioning but I opted to use grid

So I wrapped it all in a <div> container with a grid class and added two extra <div> elements. All the child <div> where given a col-start-1 and a row-start-1 so they overlay each other.

I then added bg-gradient-to-r from-white via-transparent to-20% to the two extra divs making one to-l you can also change out your background colour here, mine is just white. I also increased the z-index with z-10

I also added overflow-hidden and a rotation with -rotate-3 which you can tweak or omit.

Here is the final component...

type TickerProps = {
    images: { url: string; alt: string }[];
};

const Ticker = ({ images }: TickerProps) => {
    const ariaHidden = [false, true];

    return (
        <div className="grid">
            <div className="col-start-1 row-start-1 flex h-64 -rotate-3 overflow-hidden">
                {ariaHidden.map((hidden, idx) => (
                    <ul className="animate-image-scroll flex shrink-0 gap-8 pr-8" aria-hidden={hidden} key={idx}>
                        {images.map((image) => (
                            <li key={image.url + idx} className="shrink-0">
                                <img
                                    src={image.url}
                                    alt={image.alt}
                                    className="h-full w-[180px] object-cover opacity-85"
                                />
                            </li>
                        ))}
                    </ul>
                ))}
            </div>
            <div className="z-10 col-start-1 row-start-1 -m-1 -rotate-3 bg-gradient-to-r from-white via-transparent to-20%" />
            <div className="z-10 col-start-1 row-start-1 -m-1 -rotate-3 bg-gradient-to-l from-white via-transparent to-20%" />
        </div>
    );
};

export default Ticker;
Enter fullscreen mode Exit fullscreen mode

Top comments (0)