Marquee Effect
The marquee component is designed to create a dynamic user experience by continuously scrolling text horizontally. This effect is perfect for highlighting important information, announcements, or news headlines that need to catch the user's attention. The smooth scrolling animation adds movement and liveliness to the interface, making important content more noticeable to users. This example was created using Tailwind CSS and Framer Motion.
Examples














Installation
Install the following dependencies:
npm i motion clsx tailwind-merge react-use-measure
Add util file
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
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>
);
}
API
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactNode | The content to be scrolled inside the marquee effect | |
gap | number | 16 | The gap between each child element in the marquee |
speed | number | 100 | The speed of the scrolling animation |
speedOnHover | number | The speed of the scrolling when the user hovers over the marquee | |
direction | `"horizontal" | "vertical"` | "horizontal" |
reverse | boolean | false | Whether to reverse the direction of the marquee scroll |
className | string | Custom className for the marquee container |