Browse the library
Blocks / Sections
Logo cloud
FreeTrusted by wordmark grid with hover reveal of client outcomes and an optional two row marquee mode.
components/facade/logo-cloud.tsx
"use client";
import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import { ArrowUpRight } from "lucide-react";
import { cn } from "@/lib/utils";
export interface LogoCloudItem {
name: string;
/** Short line revealed on hover, e.g. an outcome. */
detail?: string;
href?: string;
/** Optional custom mark. Defaults to a typographic wordmark. */
logo?: React.ReactNode;
}
export interface LogoCloudProps {
eyebrow?: string;
title?: string;
description?: string;
logos?: LogoCloudItem[];
/** "grid" shows a hover-reveal grid, "marquee" scrolls logos in two rows. */
variant?: "grid" | "marquee";
/** Marquee loop duration in seconds. */
speed?: number;
className?: string;
}
const EASE = [0.22, 1, 0.36, 1] as const;
const WORDMARK_STYLES = [
"font-display font-bold tracking-tight",
"font-serif italic font-semibold",
"font-mono font-semibold uppercase tracking-[0.18em] text-[0.8em]",
"font-sans font-black uppercase tracking-tighter",
"font-display font-light uppercase tracking-[0.24em] text-[0.82em]",
"font-serif font-medium tracking-tight",
"font-sans font-extrabold tracking-[-0.06em] lowercase",
"font-display font-semibold tracking-wide",
];
const DEFAULT_LOGOS: LogoCloudItem[] = [
{ name: "Summit Roofing", detail: "+212% booked jobs" },
{ name: "Bayside Dental", detail: "4.9 stars, 860 reviews" },
{ name: "KELLER HOMES", detail: "$48M listings sold" },
{ name: "Lone Star HVAC", detail: "3x inbound calls" },
{ name: "Alder & Vine", detail: "Rebrand + new site" },
{ name: "Copperline", detail: "Top 3 in Maps" },
{ name: "brightpaws", detail: "11 locations live" },
{ name: "Harbor Realty", detail: "-38% cost per lead" },
];
function Wordmark({ item, index }: { item: LogoCloudItem; index: number }) {
if (item.logo) return <>{item.logo}</>;
return (
<span
className={cn(
"whitespace-nowrap text-base md:text-lg lg:text-2xl",
WORDMARK_STYLES[index % WORDMARK_STYLES.length],
)}
>
{item.name}
</span>
);
}
function GridCell({ item, index }: { item: LogoCloudItem; index: number }) {
const reduce = useReducedMotion();
const Tag = item.href ? "a" : "div";
return (
<motion.li
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-40px 0px" }}
transition={{ duration: 0.6, delay: reduce ? 0 : index * 0.05, ease: EASE }}
className="relative bg-background"
>
<Tag
{...(item.href ? { href: item.href, "aria-label": item.detail ? `${item.name}, ${item.detail}` : item.name } : {})}
className="group relative flex h-28 items-center justify-center overflow-hidden px-3 outline-none transition-colors hover:bg-card focus-visible:bg-card focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring sm:h-32 sm:px-4"
>
<span
aria-hidden
className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-500 group-hover:opacity-100 group-focus-visible:opacity-100"
style={{
background:
"radial-gradient(120px 80px at 50% 120%, color-mix(in oklch, var(--brand) 22%, transparent), transparent)",
}}
/>
<span className="text-foreground/40 transition-all duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:-translate-y-3 group-hover:text-foreground group-focus-visible:-translate-y-3 group-focus-visible:text-foreground">
<Wordmark item={item} index={index} />
</span>
{item.detail && (
<span className="absolute inset-x-0 bottom-4 flex translate-y-3 items-center justify-center gap-1 text-xs font-medium text-brand opacity-0 transition-all duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:translate-y-0 group-hover:opacity-100 group-focus-visible:translate-y-0 group-focus-visible:opacity-100 sm:bottom-5">
{item.detail}
{item.href && <ArrowUpRight className="size-3" aria-hidden />}
</span>
)}
</Tag>
</motion.li>
);
}
function MarqueeRow({
logos,
reverse,
speed,
offset,
}: {
logos: LogoCloudItem[];
reverse?: boolean;
speed: number;
offset: number;
}) {
return (
<div
// Decorative: the full client list is rendered once for screen readers below the rows.
aria-hidden
className="group flex overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]"
style={{
["--marquee-gap" as string]: "1rem",
["--marquee-duration" as string]: `${speed}s`,
}}
>
<ul
className={cn(
"flex w-max shrink-0 animate-marquee items-center gap-[var(--marquee-gap)] py-1 group-hover:[animation-play-state:paused]",
reverse && "[animation-direction:reverse]",
)}
>
{[...logos, ...logos].map((item, i) => (
<li
key={i}
className="flex h-16 items-center justify-center rounded-xl border border-border bg-card px-7 text-foreground/50 transition-colors hover:text-foreground"
>
<Wordmark item={item} index={(i % logos.length) + offset} />
</li>
))}
</ul>
</div>
);
}
export function LogoCloud({
eyebrow = "Trusted locally",
title = "The go-to partner for 140+ businesses across Texas",
description = "From single-location shops to regional brokerages, owners stay with us for the results.",
logos = DEFAULT_LOGOS,
variant = "grid",
speed = 40,
className,
}: LogoCloudProps) {
const half = Math.ceil(logos.length / 2);
return (
<section className={cn("bg-background py-20 text-foreground sm:py-28", className)}>
<div className="mx-auto max-w-6xl px-5 sm:px-8">
<div className="mx-auto max-w-2xl text-center">
{eyebrow && (
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-brand">
{eyebrow}
</p>
)}
{title && (
<h2 className="mt-3 font-display text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
{title}
</h2>
)}
{description && (
<p className="mt-4 text-base leading-relaxed text-muted-foreground text-pretty">
{description}
</p>
)}
</div>
{variant === "grid" ? (
<ul className="mt-14 grid grid-cols-2 gap-px overflow-hidden rounded-2xl border border-border bg-border md:grid-cols-4">
{logos.map((item, i) => (
<GridCell key={item.name} item={item} index={i} />
))}
</ul>
) : (
<div className="mt-14 flex flex-col gap-4">
<MarqueeRow logos={logos.slice(0, half)} speed={speed} offset={0} />
<MarqueeRow logos={logos.slice(half)} speed={speed * 1.15} offset={half} reverse />
<ul className="sr-only">
{logos.map((l) => (
<li key={l.name}>{l.name}</li>
))}
</ul>
</div>
)}
</div>
</section>
);
}
export default LogoCloud;app/page.tsx
import { LogoCloud } from "@/components/facade/logo-cloud";
export default function Demo() {
return (
<div>
<LogoCloud />
<LogoCloud
variant="marquee"
eyebrow="Featured partners"
title="Austin brands that grow with us"
description="Hover a row to pause it."
className="border-t border-border"
/>
</div>
);
}Installation
$
npx shadcn@latest add https://facadeui.com/r/logo-cloud.jsonUsage
import { LogoCloud } from "@/components/facade/logo-cloud";
export default function Page() {
return <LogoCloud />;
}Props
LogoCloudProps is exported, so you can extend or wrap it. Every prop has a default; <LogoCloud /> renders the example above.
| Prop | Type | Description |
|---|---|---|
| eyebrow | string | |
| title | string | |
| description | string | |
| logos | LogoCloudItem[] | |
| variant | "grid" | "marquee" | "grid" shows a hover-reveal grid, "marquee" scrolls logos in two rows. |
| speed | number | Marquee loop duration in seconds. |
| className | string |
Ask your agent
With the Facade MCP server connected, this prompt is enough:
Add the Facade logo cloud to this page and adapt the copy to our business.