bundui svg logo
Bundui

Marquee Effect

Add dynamic movement to your UI with Marquee effect, ideal for highlighting important information with smooth, continuous scrolling. Built with Tailwind CSS and Motion.

Product 1Product 2Product 3Product 4Product 5Product 6Product 7Product 1Product 2Product 3Product 4Product 5Product 6Product 7
import { MarqueeEffect } from "@/components/core/marquee-effect";

const images = Array.from({ length: 7 }, (_, i) =>
    `https://bundui-images.netlify.app/products/0${i + 1}.jpeg`
);

export default function MarqueeEffectExample() {
  return (
      <div className="grow">
        <MarqueeEffect gap={24}>
          {images.map((src, i) => (
              <img
                  key={i}
                  src={src}
                  alt={`Product ${i + 1}`}
                  className="w-32 aspect-square rounded-md"
              />
          ))}
        </MarqueeEffect>
      </div>
  );
}

Installation

Install the following dependencies:
npm install motion clsx tailwind-merge react-use-measure
Copy and paste the following code into your project:
"use client";

import React from "react";
import { cn } from "@/lib/utils";
import { useMotionValue, animate, motion } from "motion/react";
import useMeasure from "react-use-measure";

export type InfiniteSliderProps = {
  children: React.ReactNode;
  gap?: number;
  speed?: number;
  speedOnHover?: number;
  direction?: "horizontal" | "vertical";
  reverse?: boolean;
  className?: string;
};

export function MarqueeEffect({
  children,
  gap = 16,
  speed = 100,
  speedOnHover,
  direction = "horizontal",
  reverse = false,
  className,
}: InfiniteSliderProps) {
  const [currentSpeed, setCurrentSpeed] = React.useState(speed);
  const [ref, { width, height }] = useMeasure();
  const translation = useMotionValue(0);
  const [isTransitioning, setIsTransitioning] = React.useState(false);
  const [key, setKey] = React.useState(0);

  React.useEffect(() => {
    let controls;
    const size = direction === "horizontal" ? width : height;
    const contentSize = size + gap;
    const from = reverse ? -contentSize / 2 : 0;
    const to = reverse ? 0 : -contentSize / 2;

    const distanceToTravel = Math.abs(to - from);
    const duration = distanceToTravel / currentSpeed;

    if (isTransitioning) {
      const remainingDistance = Math.abs(translation.get() - to);
      const transitionDuration = remainingDistance / currentSpeed;

      controls = animate(translation, [translation.get(), to], {
        ease: "linear",
        duration: transitionDuration,
        onComplete: () => {
          setIsTransitioning(false);
          setKey((prevKey) => prevKey + 1);
        },
      });
    } else {
      controls = animate(translation, [from, to], {
        ease: "linear",
        duration: duration,
        repeat: Infinity,
        repeatType: "loop",
        repeatDelay: 0,
        onRepeat: () => {
          translation.set(from);
        },
      });
    }

    return controls?.stop;
  }, [
    key,
    translation,
    currentSpeed,
    width,
    height,
    gap,
    isTransitioning,
    direction,
    reverse,
  ]);

  const hoverProps = speedOnHover
    ? {
        onHoverStart: () => {
          setIsTransitioning(true);
          setCurrentSpeed(speedOnHover);
        },
        onHoverEnd: () => {
          setIsTransitioning(true);
          setCurrentSpeed(speed);
        },
      }
    : {};

  return (
    <div className={cn("overflow-hidden", className)}>
      <motion.div
        className="flex w-max"
        style={{
          ...(direction === "horizontal"
            ? { x: translation }
            : { y: translation }),
          gap: `${gap}px`,
          flexDirection: direction === "horizontal" ? "row" : "column",
        }}
        ref={ref}
        {...hoverProps}
      >
        {children}
        {children}
      </motion.div>
    </div>
  );
}
Update the import paths to match your project setup.

Usage

import { MarqueeEffect } from "@/components/core/marquee-effect";

const images = Array.from({ length: 7 }, (_, i) =>
    `https://bundui-images.netlify.app/products/0${i + 1}.jpeg`
);

export default function MarqueeEffectExample() {
  return (
      <div className="grow">
        <MarqueeEffect gap={24}>
          {images.map((src, i) => (
              <img
                  key={i}
                  src={src}
                  alt={`Product ${i + 1}`}
                  className="w-32 aspect-square rounded-md"
              />
          ))}
        </MarqueeEffect>
      </div>
  );
}

Examples

Reverse

Product 1Product 2Product 3Product 4Product 5Product 6Product 7Product 1Product 2Product 3Product 4Product 5Product 6Product 7
import { MarqueeEffect } from "@/components/core/marquee-effect";

const images = Array.from({ length: 7 }, (_, i) =>
    `https://bundui-images.netlify.app/products/0${i + 1}.jpeg`
);

export default function MarqueeEffectExample() {
  return (
      <div className="grow">
        <MarqueeEffect gap={24} reverse>
          {images.map((src, i) => (
              <img
                  key={i}
                  src={src}
                  alt={`Product ${i + 1}`}
                  className="w-32 aspect-square rounded-md"
              />
          ))}
        </MarqueeEffect>
      </div>
  );
}

Vertical

Product 1Product 2Product 3Product 4Product 5Product 6Product 7Product 1Product 2Product 3Product 4Product 5Product 6Product 7
import { MarqueeEffect } from "@/components/core/marquee-effect";

const images = Array.from({ length: 7 }, (_, i) =>
    `https://bundui-images.netlify.app/products/0${i + 1}.jpeg`
);

export default function MarqueeEffectExample() {
  return (
      <div className="h-80">
        <MarqueeEffect gap={24} direction="vertical">
          {images.map((src, i) => (
              <img
                  key={i}
                  src={src}
                  alt={`Product ${i + 1}`}
                  className="w-32 aspect-square rounded-md"
              />
          ))}
        </MarqueeEffect>
      </div>
  );
}

Props

PropTypeDefault
className?
string
-
reverse?
boolean
-
direction?
"horizontal" | "vertical"
"horizontal"
speedOnHover?
number
-
speed?
number
100
gap?
number
16
children
React.ReactNode
-