A Tailwind never-ending carousel (ticker)
Patrick O'Neill
Posted on July 18, 2024
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 }[];
};
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;
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',
},
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;
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;
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;
Posted on July 18, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.