Marquee
Infinite horizontal or vertical scroll with pause-on-hover.
Preview
Vercel
Linear
Stripe
Figma
GitHub
Notion
Loom
Raycast
TSXcomponents/previews/motion/marquee.preview.tsx
"use client";
import { Marquee } from "@/components/motion/marquee";
const logos = ["Vercel", "Linear", "Stripe", "Figma", "GitHub", "Notion", "Loom", "Raycast"];
export function MarqueePreview() {
return (
<div className="w-full">
<Marquee speed={25}>
{logos.map((l) => (
<div
key={l}
className="mx-4 flex h-12 items-center justify-center rounded-lg border border-border bg-card px-6 text-sm font-medium text-foreground"
>
{l}
</div>
))}
</Marquee>
</div>
);
}
TSXcomponents/motion/marquee.tsx
// beui.dev/components/motion/marquee
import { Children, type ReactNode } from "react";
import { cn } from "@/lib/utils";
export interface MarqueeProps {
children: ReactNode;
direction?: "left" | "right" | "up" | "down";
speed?: number;
pauseOnHover?: boolean;
gap?: string;
className?: string;
fade?: boolean;
}
export function Marquee({
children,
direction = "left",
speed = 30,
pauseOnHover = true,
gap = "1rem",
className,
fade = true,
}: MarqueeProps) {
const vertical = direction === "up" || direction === "down";
const reverse = direction === "right" || direction === "down";
const items = Children.toArray(children);
return (
<div
className={cn(
"group relative flex overflow-hidden",
vertical ? "flex-col" : "flex-row",
fade && !vertical && "[mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]",
fade && vertical && "[mask-image:linear-gradient(to_bottom,transparent,black_12%,black_88%,transparent)]",
className,
)}
// gap on the wrapper too, so the seam between the two tracks matches the
// spacing between items and the loop stays even.
style={{ "--gap": gap, gap } as React.CSSProperties}
>
{[0, 1].map((dup) => (
<div
key={dup}
aria-hidden={dup === 1}
inert={dup === 1}
style={{
animationDuration: `${speed}s`,
animationDirection: reverse ? "reverse" : "normal",
gap,
}}
className={cn(
"flex shrink-0 items-center",
vertical ? "flex-col animate-marquee-vertical" : "flex-row animate-marquee",
pauseOnHover && "group-hover:[animation-play-state:paused]",
)}
>
{items.map((child, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: Marquee duplicates static child slots; item order is not mutated.
<div key={i} className="shrink-0">
{child}
</div>
))}
</div>
))}
</div>
);
}
Install
Add it with the shadcn CLI, or copy the source manually.
$ bunx --bun shadcn add @beui/marquee
Needs the theme tokens once. Already ran
shadcn init? You are set. Theme setupInstall dependencies
npm i clsx tailwind-mergeAdd util file
TSXlib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy the source code
TSXcomponents/motion/marquee.tsx
// beui.dev/components/motion/marquee
import { Children, type ReactNode } from "react";
import { cn } from "@/lib/utils";
export interface MarqueeProps {
children: ReactNode;
direction?: "left" | "right" | "up" | "down";
speed?: number;
pauseOnHover?: boolean;
gap?: string;
className?: string;
fade?: boolean;
}
export function Marquee({
children,
direction = "left",
speed = 30,
pauseOnHover = true,
gap = "1rem",
className,
fade = true,
}: MarqueeProps) {
const vertical = direction === "up" || direction === "down";
const reverse = direction === "right" || direction === "down";
const items = Children.toArray(children);
return (
<div
className={cn(
"group relative flex overflow-hidden",
vertical ? "flex-col" : "flex-row",
fade && !vertical && "[mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]",
fade && vertical && "[mask-image:linear-gradient(to_bottom,transparent,black_12%,black_88%,transparent)]",
className,
)}
// gap on the wrapper too, so the seam between the two tracks matches the
// spacing between items and the loop stays even.
style={{ "--gap": gap, gap } as React.CSSProperties}
>
{[0, 1].map((dup) => (
<div
key={dup}
aria-hidden={dup === 1}
inert={dup === 1}
style={{
animationDuration: `${speed}s`,
animationDirection: reverse ? "reverse" : "normal",
gap,
}}
className={cn(
"flex shrink-0 items-center",
vertical ? "flex-col animate-marquee-vertical" : "flex-row animate-marquee",
pauseOnHover && "group-hover:[animation-play-state:paused]",
)}
>
{items.map((child, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: Marquee duplicates static child slots; item order is not mutated.
<div key={i} className="shrink-0">
{child}
</div>
))}
</div>
))}
</div>
);
}
API Reference
direction?"left" | "right" | "up" | "down"leftspeed?number30pauseOnHover?booleantruegap?string1remclassName?string—fade?booleantrueKeep in mind
Some components on this site are inspired by or recreated from existing work across the web. I'm not here to take credit; just to learn, experiment, and sometimes push things a bit further. If something looks familiar and I forgot to mention you, reach out and I'll fix that right away.
Updated