Two-row trusted-by strip of styled text wordmarks in opposing marquees that pause on hover and offscreen, with a static grid fallback.
npx shadcn@latest add @paragon/logo-cloudAlso installs: marquee
"use client";
import * as React from "react";
import { useInView, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
import { Marquee } from "@/registry/paragon/ui/marquee";
const REVEAL =
"transition-[opacity,translate,filter] duration-500 ease-out motion-reduce:transition-none";
const HIDDEN = "translate-y-3 opacity-0 blur-[4px]";
const SHOWN = "translate-y-0 opacity-100 blur-[0px]";
type Wordmark = { name: string; className: string };
/* Text wordmarks only — each gets a distinct typographic treatment so the
strip reads like real logos without shipping a single image. */
const ROW_A: Wordmark[] = [
{ name: "HALCYON", className: "text-sm font-semibold tracking-[0.25em]" },
{ name: "Verdant", className: "text-lg font-semibold tracking-tight" },
{ name: "polyphase", className: "font-mono text-sm lowercase" },
{ name: "Nimbus", className: "text-lg font-bold italic tracking-tight" },
{ name: "OSTRA", className: "text-sm font-black tracking-[0.18em]" },
{ name: "Fieldnote", className: "text-lg font-medium" },
];
const ROW_B: Wordmark[] = [
{ name: "KILOWATT", className: "font-mono text-xs tracking-[0.22em]" },
{ name: "Brightline", className: "text-lg font-semibold tracking-tight" },
{ name: "arcadia", className: "text-lg font-bold lowercase tracking-tight" },
{ name: "Summit & Co", className: "text-base font-medium italic" },
{ name: "VECTORA", className: "text-xs font-semibold tracking-[0.3em]" },
{ name: "Mooring", className: "text-lg font-semibold" },
];
function Mark({ mark }: { mark: Wordmark }) {
return (
<span
className={cn(
"whitespace-nowrap text-muted-foreground/80 select-none",
mark.className,
)}
>
{mark.name}
</span>
);
}
export interface LogoCloudProps extends React.ComponentProps<"section"> {
heading?: string;
}
/**
* "Trusted by" strip: two rows of styled text wordmarks in opposing
* infinite marquees that pause on hover and while offscreen, with edge
* fade masks. Under prefers-reduced-motion the marquees are replaced by
* a static wrapped grid of the same wordmarks.
*/
export function LogoCloud({
heading = "Trusted by data teams at",
className,
...props
}: LogoCloudProps) {
const ref = React.useRef<HTMLElement>(null);
const inView = useInView(ref, { once: true, margin: "-80px" });
const reduced = useReducedMotion();
const revealed = inView || !!reduced;
return (
<section
ref={ref}
aria-label="Customer logos"
className={cn("w-full bg-background px-4 py-12 sm:px-6 md:py-16", className)}
{...props}
>
<div className="mx-auto w-full max-w-4xl">
<p
className={cn(
"text-center text-xs font-medium tracking-wider text-muted-foreground uppercase",
REVEAL,
revealed ? SHOWN : HIDDEN,
)}
>
{heading}
</p>
{reduced ? (
/* Reduced motion: a static wrapped grid instead of a frozen marquee. */
<div className="mt-8 flex flex-wrap items-center justify-center gap-x-10 gap-y-5">
{[...ROW_A, ...ROW_B].map((mark) => (
<Mark key={mark.name} mark={mark} />
))}
</div>
) : (
<div className="mt-8 space-y-6">
<div
className={cn(REVEAL, revealed ? SHOWN : HIDDEN)}
style={{ transitionDelay: "80ms" }}
>
<Marquee duration={34} gap={56}>
{ROW_A.map((mark) => (
<Mark key={mark.name} mark={mark} />
))}
</Marquee>
</div>
<div
className={cn(REVEAL, revealed ? SHOWN : HIDDEN)}
style={{ transitionDelay: "160ms" }}
>
<Marquee duration={42} reverse gap={56}>
{ROW_B.map((mark) => (
<Mark key={mark.name} mark={mark} />
))}
</Marquee>
</div>
</div>
)}
</div>
</section>
);
}