Playground

Infinite Carousel

March 2025

"use client";

import {AnimatePresence, motion} from "motion/react";
import {useEffect, useState} from "react";

interface LogoRowProps {
    logos: (typeof items)[number][number][];
    index: number;
    activeIndex: number;
}

const items = [
    [
        "https://ik.imagekit.io/khoaphan/playground/Infinite%20Carousel/Airbnb.svg?updatedAt=1740896261205",
        "https://ik.imagekit.io/khoaphan/playground/Infinite%20Carousel/Lemon%20Squeezy%20-%20Long.svg?updatedAt=1740898819817",
        "https://ik.imagekit.io/khoaphan/playground/Infinite%20Carousel/Instagram%20-%20Long.svg?updatedAt=1740896256882",
        "https://ik.imagekit.io/khoaphan/playground/Infinite%20Carousel/Slack.svg?updatedAt=1740896256826",
    ],
    [
        "https://ik.imagekit.io/khoaphan/playground/Infinite%20Carousel/Netflix.svg?updatedAt=1740896256859",
        "https://ik.imagekit.io/khoaphan/playground/Infinite%20Carousel/Framer.svg?updatedAt=1740896256791",
        "https://ik.imagekit.io/khoaphan/playground/Infinite%20Carousel/LinkedIn%20-%20Long.svg?updatedAt=1740898819402",
        "https://ik.imagekit.io/khoaphan/playground/Infinite%20Carousel/medium.svg?updatedAt=1740896256752",
    ],
    [
        "https://ik.imagekit.io/khoaphan/playground/Infinite%20Carousel/facebook.svg?updatedAt=1740896256947",
        "https://ik.imagekit.io/khoaphan/playground/Infinite%20Carousel/Tesla.svg?updatedAt=1740896256927",
        "https://ik.imagekit.io/khoaphan/playground/Infinite%20Carousel/Mailchimp.svg?updatedAt=1740896256732",
        "https://ik.imagekit.io/khoaphan/playground/Infinite%20Carousel/Microsoft.svg?updatedAt=1740896256652",
    ],
];

export default function InfiniteCarousel() {
    const [activeIndex, setActiveIndex] = useState(0);

    useEffect(() => {
        const nextIndex =
            activeIndex === items.length - 1 ? 0 : activeIndex + 1;
        const intervalId = setInterval(() => setActiveIndex(nextIndex), 3000);

        return () => clearInterval(intervalId);
    }, [activeIndex]);

    return (
        <article className="relative grid min-h-80 place-items-center rounded-xl border border-white/10 bg-white/5 p-3 md:aspect-[2] md:min-h-0">
            <div className="relative h-40 w-full">
                {items.map((logos, index) => (
                    <LogoRow
                        key={index}
                        activeIndex={activeIndex}
                        logos={logos}
                        index={index}
                    />
                ))}
            </div>
        </article>
    );
}

const LogoRow = ({logos, index, activeIndex}: LogoRowProps) => {
    const isActive = index === activeIndex;

    return (
        <AnimatePresence>
            {isActive && (
                <div className="absolute left-0 top-0 grid h-full w-full md:gap-x-6 md:px-8 grid-cols-4 gap-3 px-5">
                    {logos.map((logo, logoIndex) => (
                        <motion.div
                            key={logoIndex}
                            initial={{y: 40, opacity: 0, filter: "blur(10px)"}}
                            animate={{
                                y: 0,
                                opacity: 1,
                                filter: "blur(0px)",
                            }}
                            exit={{y: -40, opacity: 0, filter: "blur(10px)"}}
                            transition={{
                                duration: 1.5,
                                type: "spring",
                                delay: logoIndex * 0.15,
                            }}>
                            <img
                                src={logo}
                                loading="lazy"
                                className="h-full w-full object-contain"
                            />
                        </motion.div>
                    ))}
                </div>
            )}
        </AnimatePresence>
    );
};
Previous

Button Effects

Next

Momentum Lines