Carousel.tsx

import React from "react";
export default function Carousel() {
const ref = React.useRef<HTMLDivElement>(null);
const [index, setIndex] = React.useState(0);
const [numberOfImages, setNumberOfImages] = React.useState(3);
React.useEffect(() => {
if (!ref.current) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
if ((entry.target as HTMLImageElement).complete === false) {
return;
}
const index = (entry.target as HTMLImageElement).dataset.index;
if (index) {
setIndex(Number(index));
}
}
});
},
{
root: ref.current,
threshold: 1,
}
);
const children = ref.current.children;
for (let i = 0; i < children.length; i++) {
(children[i] as HTMLImageElement).dataset.index = i.toString();
observer.observe(children[i]);
}
return () => {
observer.disconnect();
};
}, [numberOfImages]);
return (
<>
<section className="relative">
<button
onClick={() => ref.current?.scrollBy(-1000, 0)}
className="absolute top-1/2 left-0 text-4xl"
>
⬅️
</button>
<button
onClick={() => ref.current?.scrollBy(1000, 0)}
className="absolute top-1/2 right-0 text-4xl"
>
➡️
</button>
<nav className="absolute bottom-8 flex justify-center w-full">
<ul className="flex gap-2 text-7xl text-gray-400 font-thin">
{[...Array(numberOfImages)].map((_, i) => {
return (
<li key={i}>
<button
className={i === index ? "text-white" : ""}
onClick={() => {
ref.current?.scrollTo({
left: i * 1000,
});
}}
>
_
</button>
</li>
);
})}
</ul>
</nav>
<div
ref={ref}
className="scroll-smooth overflow-x-scroll whitespace-nowrap snap-x snap-mandatory"
style={{ scrollbarWidth: "none" }}
// wait for scrollbarwidth to be supported to hide scrollbar
>
{[...Array(numberOfImages)].map((_, i) => {
return (
<img
src="//unsplash.it/1000/400"
className="inline-block snap-start"
key={i}
onLoad={() => ref.current?.scrollTo({ left: 0 })}
// images are loaded at different orders, so we need to somehow start at the first image by using this hack
/>
);
})}
</div>
</section>
<label>
Number of Images
<input
type="range"
min={1}
max={6}
value={numberOfImages}
onChange={(e) => setNumberOfImages(Number(e.target.value))}
/>
</label>
</>
);
}