UI/UI
Marquee
A smooth scrolling marquee component for displaying continuous content.
React•Next.js•TypeScript•Tailwind CSS•
<Marquee>
<div className="flex gap-8 text-sm">
<span>React</span>
<span>•</span>
<span>Next.js</span>
<span>•</span>
<span>TypeScript</span>
<span>•</span>
<span>Tailwind CSS</span>
<span>•</span>
</div>
</Marquee>
Installation
@theme {
--animate-marquee: marquee 30s linear infinite;
--animate-marqueeY: marqueeY 200s linear infinite;
}
@keyframes marquee {
0% {
transform: translateX(0%);
}
100% {
transform: translateX(calc(-100% - var(--gap)));
}
}
@keyframes marqueeY {
0% {
transform: translateY(0%);
}
100% {
transform: translateY(calc(-100% - var(--gap)));
}
}
module.exports = {
theme: {
extend: {
keyframes: {
"marquee": {
from: {
transform: "translateX(0)",
},
to: {
transform: "translateX(-100% - var(--gap)))",
},
},
"marqueeY": {
from: {
transform: "translateY(0)",
},
to: {
transform: "translateY(-100% - var(--gap)))",
},
},
}
animations: {
marquee: "marquee 30s linear infinite",
marqueeY: "marqueeY 200s linear infinite",
},
},
}
}
}
Copy and paste the following code into your project.
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
export interface MarqueeProps {
children: React.ReactNode;
direction?: "left" | "right" | "up" | "down";
speed?: "slow" | "normal" | "fast" | number;
pauseOnHover?: boolean;
repeat?: number;
gap?: string | number;
fade?: boolean;
className?: string;
style?: React.CSSProperties;
vertical?: boolean;
autoFill?: boolean;
"aria-label"?: string;
}
export const Marquee = React.forwardRef<HTMLDivElement, MarqueeProps>(
(
{
children,
direction = "left",
speed = "normal",
pauseOnHover = true,
repeat = 4,
gap = "1rem",
fade = true,
className,
style,
vertical = false,
autoFill = false,
"aria-label": ariaLabel,
...props
},
ref
) => {
const containerRef = React.useRef<HTMLDivElement>(null);
const [isClient, setIsClient] = React.useState(false);
const [containerWidth, setContainerWidth] = React.useState(0);
const [contentWidth, setContentWidth] = React.useState(0);
React.useEffect(() => {
setIsClient(true);
}, []);
React.useEffect(() => {
const container = containerRef.current;
if (!container || !isClient) return;
const resizeObserver = new ResizeObserver(() => {
setContainerWidth(container.offsetWidth);
const firstChild = container.firstElementChild as HTMLElement;
if (firstChild) {
setContentWidth(firstChild.scrollWidth);
}
});
resizeObserver.observe(container);
return () => resizeObserver.disconnect();
}, [isClient, children]);
const getSpeed = (): string => {
if (typeof speed === "number") {
return `${speed}s`;
}
const speeds = {
slow: "60s",
normal: "30s",
fast: "15s",
};
return speeds[speed];
};
const getAnimationName = (): string => {
if (vertical || direction === "up" || direction === "down") {
return "marqueeY";
}
return "marquee";
};
const getAnimationDirection = (): string => {
if (direction === "right" || direction === "down") {
return "reverse";
}
return "normal";
};
const calculateRepeat = (): number => {
if (!autoFill || !isClient) return repeat;
if (containerWidth && contentWidth) {
return Math.ceil(containerWidth / contentWidth) + 1;
}
return repeat;
};
const gapValue = typeof gap === "number" ? `${gap}px` : gap;
const containerStyles: React.CSSProperties = {
"--gap": gapValue,
"--duration": getSpeed(),
...style,
} as React.CSSProperties;
const animationStyles: React.CSSProperties = {
animationName: getAnimationName(),
animationDuration: getSpeed(),
animationTimingFunction: "linear",
animationIterationCount: "infinite",
animationDirection: getAnimationDirection(),
animationPlayState: "running",
};
const fadeStyles = fade
? vertical
? {
maskImage:
"linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,1) 10%, rgba(0,0,0,1) 90%, rgba(0,0,0,0))",
WebkitMaskImage:
"linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,1) 10%, rgba(0,0,0,1) 90%, rgba(0,0,0,0))",
}
: {
maskImage:
"linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,1) 10%, rgba(0,0,0,1) 90%, rgba(0,0,0,0))",
WebkitMaskImage:
"linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,1) 10%, rgba(0,0,0,1) 90%, rgba(0,0,0,0))",
}
: {};
const content = Array.from({ length: calculateRepeat() }, (_, i) => (
<div
key={i}
className={cn(
"flex shrink-0",
vertical ? "flex-col" : "flex-row",
"[gap:var(--gap)]"
)}
style={animationStyles}
aria-hidden={i > 0 ? "true" : undefined}
>
{children}
</div>
));
return (
<div
ref={ref}
role="marquee"
aria-label={ariaLabel || "Scrolling content"}
aria-live="off"
className={cn(
"group flex overflow-hidden",
vertical ? "flex-col" : "flex-row",
pauseOnHover && "hover:[&>*]:pause-animation",
"motion-reduce:hover:[&>*]:pause-animation",
className
)}
style={{
...containerStyles,
...fadeStyles,
}}
{...props}
>
<div
ref={containerRef}
className={cn(
"flex",
vertical ? "flex-col" : "flex-row",
"[gap:var(--gap)]"
)}
>
{content}
</div>
</div>
);
}
);
Marquee.displayName = "Marquee";
// Convenience components for common use cases
export const MarqueeItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex shrink-0 items-center justify-center", className)}
{...props}
/>
));
MarqueeItem.displayName = "MarqueeItem";
export const MarqueeContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center gap-4", className)}
{...props}
/>
));
MarqueeContent.displayName = "MarqueeContent";
npx hextaui@latest add marquee
pnpm dlx hextaui@latest add marquee
yarn dlx hextaui@latest add marquee
bun x hextaui@latest add marquee
Usage
import { Marquee } from "@/components/ui/marquee";
<Marquee>
<div className="flex gap-4">
<span>Content 1</span>
<span>Content 2</span>
<span>Content 3</span>
</div>
</Marquee>
Examples
Brand Logos
React
Next.js
Tailwind
Node.js
<Marquee speed="normal">
<div className="flex items-center gap-8">
<img src="/logo1.svg" alt="Company 1" className="h-8" />
<img src="/logo2.svg" alt="Company 2" className="h-8" />
<img src="/logo3.svg" alt="Company 3" className="h-8" />
<img src="/logo4.svg" alt="Company 4" className="h-8" />
</div>
</Marquee>
News Ticker
BREAKING
📈 Markets surge after tech announcement•🌍 Climate summit reaches agreement•🚀 New space mission launched•
<div className="bg-red-600 text-white rounded-lg overflow-hidden">
<div className="bg-red-700 px-3 py-1">
<span className="font-bold text-sm">BREAKING</span>
</div>
<Marquee className="py-2">
<div className="flex gap-4 text-sm px-4">
<span>📈 Markets surge after tech announcement</span>
<span>•</span>
<span>🌍 Climate summit reaches agreement</span>
<span>•</span>
<span>🚀 New space mission launched</span>
<span>•</span>
</div>
</Marquee>
</div>
Image Gallery
<Marquee speed="normal" className="py-4">
<div className="flex items-center gap-6">
<img
src="https://images.unsplash.com/photo-1517180102446-f3ece451e9d8?w=200&h=120&fit=crop&crop=center"
alt="Modern office workspace"
className="w-[20rem] h-auto object-cover rounded-lg"
/>
<img
src="https://images.unsplash.com/photo-1515378960530-7c0da6231fb1?w=200&h=120&fit=crop&crop=center"
alt="Computer setup"
className="w-[20rem] h-auto object-cover rounded-lg"
/>
<img
src="https://images.unsplash.com/photo-1516259762381-22954d7d3ad2?w=200&h=120&fit=crop&crop=center"
alt="Coffee and laptop"
className="w-[20rem] h-auto object-cover rounded-lg"
/>
<img
src="https://images.unsplash.com/photo-1618477388954-7852f32655ec?w=200&h=120&fit=crop&crop=center"
alt="Tech workspace"
className="w-[20rem] h-auto object-cover rounded-lg"
/>
<img
src="https://images.unsplash.com/photo-1531297484001-80022131f5a1?w=200&h=120&fit=crop&crop=center"
alt="Technology concept"
className="w-[20rem] h-auto object-cover rounded-lg"
/>
<img
src="https://images.unsplash.com/photo-1555949963-aa79dcee981c?w=200&h=120&fit=crop&crop=center"
alt="Developer workspace"
className="w-[20rem] h-auto object-cover rounded-lg"
/>
</div>
</Marquee>
User Marquee
<Marquee speed="normal" className="py-4">
<div className="flex items-center gap-4">
{['John', 'Sarah', 'Mike', 'Emma', 'Alex', 'Lisa', 'David', 'Kate'].map((name) => (
<div key={name} className="flex flex-col items-center gap-2">
<img
src={`https://api.dicebear.com/7.x/micah/svg?seed=${name}`}
alt={`${name}'s avatar`}
className="w-12 h-12 rounded-full"
/>
<span className="text-xs font-medium">{name}</span>
</div>
))}
</div>
</Marquee>
Props
Prop | Type | Default |
---|---|---|
children? | ReactNode | undefined |
className? | string | undefined |
pauseOnHover? | boolean | true |
speed? | "slow" | "normal" | "fast" | "normal" |
direction? | "left" | "right" | "left" |
Edit on GitHub
Last updated on