freeNew156 lines · framer-motion
Logo Cloud Marquee
A scrolling row of marks, drawn, not borrowed. An infinite horizontal marquee of abstract, hand-drawn SVG marks in bordered chips — a 'works with' row with none of the weight (or legal risk) of real logos.
- Use it for
- The social-proof row under a hero, before you have real integration or customer logos to show, or when you never want to depend on anyone else's brand assets.
- Themes
- Light + dark
- License
- Commercial use
Live
Works with
Plugs into the stack you already run
tsx
"use client";
import * as React from "react";
import { useReducedMotion } from "framer-motion";
import { cn } from "@/lib/utils";
function useReducedMotionSafe() {
const prefersReduced = useReducedMotion();
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => setMounted(true), []);
return mounted ? !!prefersReduced : false;
}
export interface LogoMark {
/** Which abstract SVG glyph to draw. */
glyph: "orbit" | "prism" | "weave" | "node" | "arc" | "stack" | "spiral" | "flux";
/** Relative width multiplier, 1 = default. */
width?: number;
}
export interface LogoCloudMarqueeProps {
eyebrow?: string;
title?: string;
logos?: LogoMark[];
speedSeconds?: number;
className?: string;
}
const DEFAULT_LOGOS: LogoMark[] = [
{ glyph: "orbit" },
{ glyph: "prism", width: 1.1 },
{ glyph: "weave" },
{ glyph: "node", width: 0.9 },
{ glyph: "arc" },
{ glyph: "stack", width: 1.05 },
{ glyph: "spiral" },
{ glyph: "flux", width: 0.95 },
];
/**
* Each glyph is a fully abstract, hand-drawn mark — no letterforms, no resemblance
* to any real or invented brand. They exist purely to fill a "works with" row with
* motion and shape instead of grey boxes.
*/
function Glyph({ kind }: { kind: LogoMark["glyph"] }) {
const common = { viewBox: "0 0 32 32", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" as const, strokeLinejoin: "round" as const };
switch (kind) {
case "orbit":
return (
<svg {...common} className="size-7">
<circle cx="16" cy="16" r="3.2" fill="currentColor" stroke="none" />
<ellipse cx="16" cy="16" rx="13" ry="6" />
<ellipse cx="16" cy="16" rx="6" ry="13" transform="rotate(35 16 16)" />
</svg>
);
case "prism":
return (
<svg {...common} className="size-7">
<path d="M16 4 27 23H5Z" />
<path d="M16 4 16 17M27 23 16 17M5 23 16 17" />
</svg>
);
case "weave":
return (
<svg {...common} className="size-7">
<path d="M4 10c5 0 5 6 10 6s5-6 10-6" />
<path d="M4 16c5 0 5 6 10 6s5-6 10-6" />
</svg>
);
case "node":
return (
<svg {...common} className="size-7">
<circle cx="8" cy="9" r="2.6" />
<circle cx="24" cy="9" r="2.6" />
<circle cx="16" cy="23" r="2.6" />
<path d="M10.3 10.6 13.8 20.6M21.7 10.6 18.2 20.6M10.6 9h10.8" />
</svg>
);
case "arc":
return (
<svg {...common} className="size-7">
<path d="M6 23c0-9 4.5-15 10-15s10 6 10 15" />
<path d="M11 23c0-6 2.5-10 5-10s5 4 5 10" />
</svg>
);
case "stack":
return (
<svg {...common} className="size-7">
<path d="m16 5 11 5.5L16 16 5 10.5Z" />
<path d="m5 17 11 5.5L27 17M5 23.5 16 29l11-5.5" />
</svg>
);
case "spiral":
return (
<svg {...common} className="size-7">
<path d="M16 16c3 0 5-2 5-5s-3-5-6-5-7 2.5-7 7 3.5 8 8 8 9-3.5 9-9" />
</svg>
);
case "flux":
return (
<svg {...common} className="size-7">
<path d="M6 10h11a5 5 0 0 1 0 10H9" />
<path d="M26 22H15a5 5 0 0 1 0-10h8" />
<path d="m9 7-3 3 3 3M23 19l3 3-3 3" />
</svg>
);
}
}
function LogoChip({ glyph, width = 1 }: LogoMark) {
return (
<div
className="flex h-14 shrink-0 items-center justify-center rounded-xl border border-border bg-muted/40 px-6 text-muted-foreground transition-colors hover:border-foreground/40 hover:text-foreground"
style={{ width: `${7.5 * width}rem` }}
>
<Glyph kind={glyph} />
</div>
);
}
export function LogoCloudMarquee({ eyebrow = "Works with", title = "Plugs into the stack you already run", logos = DEFAULT_LOGOS, speedSeconds = 30, className }: LogoCloudMarqueeProps) {
const reduce = useReducedMotionSafe();
const doubled = [...logos, ...logos];
return (
<section className={cn("w-full overflow-hidden bg-background py-16", className)}>
<div className="mx-auto max-w-3xl px-4 text-center sm:px-6 lg:px-10">
<p className="font-mono text-[11px] uppercase tracking-[0.2em] text-muted-foreground">{eyebrow}</p>
<h2 className="mt-3 font-display text-[clamp(1.6rem,3.6vw,2.4rem)] font-extrabold leading-[0.95] tracking-[-0.03em] text-foreground">{title}</h2>
</div>
<div className="group relative mt-10 overflow-hidden [mask-image:linear-gradient(90deg,transparent,black_8%,black_92%,transparent)]">
<div
className="flex w-max items-center gap-4 [animation-name:lk-logo-marquee] [animation-timing-function:linear] [animation-iteration-count:infinite] group-hover:[animation-play-state:paused] motion-reduce:[animation-play-state:paused]"
style={{ animationDuration: `${speedSeconds}s` }}
>
{doubled.map((l, i) => (
<LogoChip key={i} {...l} />
))}
</div>
</div>
{!reduce && (
<style>{`
@keyframes lk-logo-marquee {
from { transform: translateX(0%); }
to { transform: translateX(-50%); }
}
`}</style>
)}
</section>
);
}
export default LogoCloudMarquee;
How it behaves
- →Eight distinct abstract line-art glyphs (orbit, prism, weave, node, arc, stack, spiral, flux) — no letterforms, no resemblance to any real or invented brand.
- →The row duplicates its items and scrolls on a CSS keyframe animation at a constant speed set by speedSeconds.
- →The row pauses on hover and is masked with a fade at both edges so items never pop in or out abruptly.
- →prefers-reduced-motion pauses the scroll entirely, leaving a static row.
Accessibility
- ✓Marks are purely decorative; the section heading carries the meaning.
- ✓Scrolling pauses under prefers-reduced-motion and on hover/focus-within.
- ✓Honours prefers-reduced-motion.