Browse the library
Components / Layout
Marquee
FreeSeamless infinite marquee for logos or testimonials with faded edges, direction, speed and vertical mode.
components/facade/marquee.tsx
import * as React from "react";
import { cn } from "@/lib/utils";
export interface MarqueeProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
/** Seconds for one full loop. Lower is faster. Defaults to 30. */
speed?: number;
/** Scroll direction. In vertical mode "left" scrolls up and "right" scrolls down. Defaults to "left". */
direction?: "left" | "right";
/** Pause the animation while hovered. Defaults to true. */
pauseOnHover?: boolean;
/** Fade the edges with a mask-image. Defaults to true. */
fade?: boolean;
/** Scroll vertically instead of horizontally. */
vertical?: boolean;
/** Gap between items as a CSS length. Defaults to "3rem". */
gap?: string;
/**
* How many times children are repeated inside each copy of the track.
* Raise it when your content is narrower than the container. Defaults to 1.
*/
repeat?: number;
/** Class applied to each copy of the content. */
trackClassName?: string;
}
const VERTICAL_KEYFRAMES =
"@keyframes facade-marquee-y{from{transform:translateY(0)}to{transform:translateY(calc(-50% - var(--marquee-gap, 1rem) / 2))}}";
/**
* Infinite marquee. Children are rendered in two identical copies inside one
* track, which `animate-marquee` (globals.css) shifts by exactly one copy.
*/
export function Marquee({
children,
speed = 30,
direction = "left",
pauseOnHover = true,
fade = true,
vertical = false,
gap = "3rem",
repeat = 1,
className,
trackClassName,
style,
...rest
}: MarqueeProps) {
const mask = vertical
? "linear-gradient(to bottom, transparent, black 14%, black 86%, transparent)"
: "linear-gradient(to right, transparent, black 12%, black 88%, transparent)";
const rootStyle = {
"--marquee-duration": `${speed}s`,
"--marquee-gap": gap,
...(fade ? { maskImage: mask, WebkitMaskImage: mask } : {}),
...style,
} as React.CSSProperties;
const trackStyle: React.CSSProperties = {
gap: "var(--marquee-gap)",
animationDirection: direction === "right" ? "reverse" : "normal",
...(vertical ? { animation: "facade-marquee-y var(--marquee-duration, 30s) linear infinite" } : {}),
};
const copy = (hidden: boolean) => (
<div
aria-hidden={hidden || undefined}
className={cn("flex shrink-0 items-center", vertical ? "flex-col" : "flex-row", trackClassName)}
style={{ gap: "var(--marquee-gap)" }}
>
{Array.from({ length: Math.max(1, repeat) }, (_, i) => (
<React.Fragment key={i}>{children}</React.Fragment>
))}
</div>
);
return (
<div
{...rest}
style={rootStyle}
className={cn("group/marquee relative flex overflow-hidden", vertical ? "flex-col" : "flex-row", className)}
>
{vertical ? <style>{VERTICAL_KEYFRAMES}</style> : null}
<div
style={trackStyle}
className={cn(
"flex shrink-0",
vertical ? "h-max flex-col" : "w-max flex-row animate-marquee",
"motion-reduce:[animation-play-state:paused]",
pauseOnHover && "group-hover/marquee:[animation-play-state:paused] group-focus-within/marquee:[animation-play-state:paused]",
)}
>
{copy(false)}
{copy(true)}
</div>
</div>
);
}
export default Marquee;app/page.tsx
import { Coffee, Dumbbell, Home, Key, Scissors, Sun, Trees, Utensils, Wrench } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { Marquee } from "@/components/facade/marquee";
interface Brand {
name: string;
icon: LucideIcon;
style: string;
}
const row1: Brand[] = [
{ name: "Juniper & Co.", icon: Trees, style: "font-display font-semibold tracking-tight" },
{ name: "HEARTH HOMES", icon: Home, style: "font-semibold tracking-[0.2em] text-[13px]" },
{ name: "Copperline", icon: Wrench, style: "font-mono font-medium tracking-tight" },
{ name: "Saltwater Coffee", icon: Coffee, style: "font-display italic font-medium" },
{ name: "Ironside Gym", icon: Dumbbell, style: "font-bold uppercase tracking-tight" },
];
const row2: Brand[] = [
{ name: "Mesa Realty", icon: Key, style: "font-display font-semibold tracking-tight" },
{ name: "Golden Hour Solar", icon: Sun, style: "font-medium tracking-tight" },
{ name: "Fade Society", icon: Scissors, style: "font-bold uppercase tracking-[0.12em] text-[13px]" },
{ name: "Olive & Ember", icon: Utensils, style: "font-display italic font-semibold" },
{ name: "Northline Builders", icon: Home, style: "font-mono font-semibold uppercase tracking-tight text-[13px]" },
];
function Wordmark({ brand }: { brand: Brand }) {
return (
<span className="inline-flex items-center gap-2 whitespace-nowrap text-lg text-foreground/55 transition-colors duration-300 hover:text-foreground">
<brand.icon className="size-5" strokeWidth={1.75} />
<span className={brand.style}>{brand.name}</span>
</span>
);
}
export default function Demo() {
return (
<section className="w-full max-w-3xl">
<p className="text-center text-sm text-muted-foreground">
Trusted by <span className="font-medium text-foreground">340+ local businesses</span> across Texas and Colorado
</p>
<div className="mt-7 space-y-5">
<Marquee speed={36} repeat={2}>
{row1.map((b) => (
<Wordmark key={b.name} brand={b} />
))}
</Marquee>
<Marquee speed={42} direction="right" repeat={2}>
{row2.map((b) => (
<Wordmark key={b.name} brand={b} />
))}
</Marquee>
</div>
</section>
);
}Installation
$
npx shadcn@latest add https://facadeui.com/r/marquee.jsonUsage
import { Marquee } from "@/components/facade/marquee";
export default function Page() {
return <Marquee />;
}Props
MarqueeProps is exported, so you can extend or wrap it. Every prop has a default; <Marquee /> renders the example above.
| Prop | Type | Description |
|---|---|---|
| children | React.ReactNode | |
| speed | number | Seconds for one full loop. Lower is faster. Defaults to 30. |
| direction | "left" | "right" | Scroll direction. In vertical mode "left" scrolls up and "right" scrolls down. Defaults to "left". |
| pauseOnHover | boolean | Pause the animation while hovered. Defaults to true. |
| fade | boolean | Fade the edges with a mask-image. Defaults to true. |
| vertical | boolean | Scroll vertically instead of horizontally. |
| gap | string | Gap between items as a CSS length. Defaults to "3rem". |
| repeat | number | How many times children are repeated inside each copy of the track. Raise it when your content is narrower than the container. Defaults to 1. |
| trackClassName | string | Class applied to each copy of the content. |
Ask your agent
With the Facade MCP server connected, this prompt is enough:
Add the Facade marquee to this page and adapt the copy to our business.