Sections

Logo Cloud

Two-row trusted-by strip of styled text wordmarks in opposing marquees that pause on hover and offscreen, with a static grid fallback.

Install

npx shadcn@latest add @paragon/logo-cloud

Also installs: marquee

logo-cloud.tsx

"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>
  );
}