Logo Cloud
Trusted-by logo strip with an infinite marquee variant behind edge fade masks that pauses on hover or focus, and a grid variant whose logos fade in with a stagger and lift on hover; logos start muted and grayscale and take their own colors on hover, accept any SVG or node, and fall back to a static row under reduced motion.
Preview
Install
Add it with the shadcn CLI, or copy the source manually.
shadcn init? You are set. Theme setupInstall dependencies
npm i clsx motion tailwind-mergeAdd util files
// Shared motion tokens. Easing curves mirror the CSS custom properties in
// globals.css; springs are the canonical physics used across components.
// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.
export const EASE_OUT = [0.16, 1, 0.3, 1] as const;
export const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;
export const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;
/** CSS string form of EASE_OUT for inline style transitions. */
export const EASE_OUT_CSS = "cubic-bezier(0.16, 1, 0.3, 1)";
/** Press feedback on buttons and other tappable surfaces. */
export const SPRING_PRESS = {
type: "spring",
stiffness: 500,
damping: 30,
mass: 0.6,
} as const;
/** Content swaps — label/icon slots trading places inside a control. */
export const SPRING_SWAP = {
type: "spring",
stiffness: 460,
damping: 30,
mass: 0.55,
} as const;
/** Overlay panel entrances — modals and sheets summoned by pointer. */
export const SPRING_PANEL = {
type: "spring",
stiffness: 420,
damping: 40,
mass: 0.5,
} as const;
/** Shared-layout glides — pills, indicators and panels morphing between positions. */
export const SPRING_LAYOUT = {
type: "spring",
stiffness: 360,
damping: 32,
mass: 0.6,
} as const;
/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */
export const SPRING_MOUSE = {
stiffness: 200,
damping: 15,
mass: 0.3,
} as const;
/** Dragged handles and fills (sliders) — critically damped `useSpring` config,
* so the value follows the pointer butterily and never rebounds off an end. */
export const SPRING_GLIDE = {
stiffness: 700,
damping: 50,
mass: 0.5,
} as const;
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy the source code
"use client";
// karaui.kamisama-v0.my.id/components/blocks/logo-cloud
import { motion, useReducedMotion } from "motion/react";
import type { ReactNode } from "react";
import { Marquee } from "@/components/motion/marquee";
import { EASE_OUT } from "@/lib/ease";
import { cn } from "@/lib/utils";
export interface LogoCloudItem {
id: string;
/** Accessible name of the company or product. */
name: string;
/** SVG or any node. Use `currentColor` so it follows the muted/hover tint. */
logo: ReactNode;
href?: string;
}
export interface LogoCloudProps {
logos: LogoCloudItem[];
/** "marquee" scrolls one infinite row; "grid" lays logos out and fades them in. Default "marquee". */
variant?: "marquee" | "grid";
eyebrow?: ReactNode;
heading?: ReactNode;
description?: ReactNode;
/** Seconds for one marquee loop. Default 30. */
speed?: number;
/** Marquee direction. Default "left". */
direction?: "left" | "right";
/** Pause the marquee on hover or keyboard focus. Default true. */
pauseOnHover?: boolean;
/** Logos start muted (grayscale, dimmed) and take their own colors on hover. Default true. */
colorize?: boolean;
/** Grid columns at the widest container size. Default 4. */
columns?: 3 | 4 | 5 | 6;
className?: string;
/** Classes for each logo tile (sizing, padding). */
itemClassName?: string;
}
// Static class maps: Tailwind only ships classes it can see written out.
const GRID_COLUMNS: Record<3 | 4 | 5 | 6, string> = {
3: "grid-cols-2 @md:grid-cols-3",
4: "grid-cols-2 @md:grid-cols-4",
5: "grid-cols-2 @md:grid-cols-3 @2xl:grid-cols-5",
6: "grid-cols-3 @md:grid-cols-4 @2xl:grid-cols-6",
};
const STAGGER = 0.05;
function LogoMark({
item,
colorize,
lift,
className,
}: {
item: LogoCloudItem;
colorize: boolean;
lift: boolean;
className?: string;
}) {
const classes = cn(
"group/logo flex h-12 items-center justify-center px-3 text-muted-foreground outline-none transition-[color,filter,opacity,translate,border-color,box-shadow] duration-300 [&_svg]:h-7 [&_svg]:w-auto [&_svg]:max-w-full",
colorize &&
"opacity-70 grayscale hover:text-foreground hover:opacity-100 hover:grayscale-0 focus-visible:text-foreground focus-visible:opacity-100 focus-visible:grayscale-0",
// Tailwind's hover variant only applies on hover-capable pointers, so
// touch devices never get a stuck lift.
lift && "hover:-translate-y-1 motion-reduce:hover:translate-y-0",
item.href && "rounded-xl focus-visible:ring-2 focus-visible:ring-ring/50",
className,
);
const content = (
<>
<span aria-hidden className="contents">
{item.logo}
</span>
<span className="sr-only">{item.name}</span>
</>
);
return item.href ? (
<a href={item.href} className={classes}>
{content}
</a>
) : (
<div className={classes}>{content}</div>
);
}
/**
* "Trusted by" logo strip. The marquee variant reuses `Marquee` for one
* infinite row behind edge fade masks, pausing on hover or focus. The grid
* variant fades logos in with a stagger when it scrolls into view; each
* logo lifts on hover. Logos start muted and take their own colors on hover.
* Reduced motion shows the marquee's logos as a static wrapped row.
*/
export function LogoCloud({
logos,
variant = "marquee",
eyebrow,
heading,
description,
speed = 30,
direction = "left",
pauseOnHover = true,
colorize = true,
columns = 4,
className,
itemClassName,
}: LogoCloudProps) {
const reduce = useReducedMotion();
const header =
eyebrow || heading || description ? (
<div className="mx-auto mb-6 flex max-w-xl flex-col items-center gap-2 text-center">
{eyebrow ? (
<p className="text-xs font-medium uppercase tracking-[0.2em] text-muted-foreground">
{eyebrow}
</p>
) : null}
{heading ? (
<h2 className="text-balance font-display text-xl font-semibold leading-tight tracking-tight text-foreground @3xl:text-2xl">
{heading}
</h2>
) : null}
{description ? (
<p className="text-pretty text-sm leading-6 text-muted-foreground">
{description}
</p>
) : null}
</div>
) : null;
let body: ReactNode;
if (variant === "marquee" && !reduce) {
body = (
<Marquee
direction={direction}
speed={speed}
pauseOnHover={pauseOnHover}
gap="1.5rem"
className={cn(
"py-2",
pauseOnHover && "[&:focus-within>div]:[animation-play-state:paused]",
)}
>
{logos.map((item) => (
<LogoMark
key={item.id}
item={item}
colorize={colorize}
lift={false}
className={itemClassName}
/>
))}
</Marquee>
);
} else if (variant === "marquee") {
// Reduced motion: no endless movement, just a centered wrapped row.
body = (
<ul className="flex flex-wrap items-center justify-center gap-x-6 gap-y-3">
{logos.map((item) => (
<li key={item.id}>
<LogoMark item={item} colorize={colorize} lift={false} className={itemClassName} />
</li>
))}
</ul>
);
} else {
body = (
<motion.ul
initial="hidden"
whileInView="show"
viewport={{ once: true, amount: 0.3 }}
variants={{ show: { transition: { staggerChildren: STAGGER } } }}
className={cn("grid gap-3", GRID_COLUMNS[columns])}
>
{logos.map((item) => (
<motion.li
key={item.id}
variants={{
hidden: reduce ? { opacity: 0 } : { opacity: 0, y: 10, scale: 0.96 },
show: {
opacity: 1,
y: 0,
scale: 1,
transition: { duration: 0.5, ease: EASE_OUT },
},
}}
>
<LogoMark
item={item}
colorize={colorize}
lift
className={cn(
"h-20 rounded-2xl border border-border bg-card shadow-xs hover:border-border-strong hover:shadow-sm",
itemClassName,
)}
/>
</motion.li>
))}
</motion.ul>
);
}
return (
<section className={cn("@container w-full", className)}>
{header}
{body}
</section>
);
}
import { Children, type ReactNode } from "react";
import { cn } from "@/lib/utils";
export interface MarqueeProps {
children: ReactNode;
direction?: "left" | "right" | "up" | "down";
speed?: number;
pauseOnHover?: boolean;
gap?: string;
className?: string;
fade?: boolean;
}
export function Marquee({
children,
direction = "left",
speed = 30,
pauseOnHover = true,
gap = "1rem",
className,
fade = true,
}: MarqueeProps) {
const vertical = direction === "up" || direction === "down";
const reverse = direction === "right" || direction === "down";
const items = Children.toArray(children);
return (
<div
className={cn(
"group relative flex overflow-hidden",
vertical ? "flex-col" : "flex-row",
fade && !vertical && "[mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]",
fade && vertical && "[mask-image:linear-gradient(to_bottom,transparent,black_12%,black_88%,transparent)]",
className,
)}
// gap on the wrapper too, so the seam between the two tracks matches the
// spacing between items and the loop stays even.
style={{ "--gap": gap, gap } as React.CSSProperties}
>
{[0, 1].map((dup) => (
<div
key={dup}
aria-hidden={dup === 1}
inert={dup === 1}
style={{
animationDuration: `${speed}s`,
animationDirection: reverse ? "reverse" : "normal",
gap,
}}
className={cn(
"flex shrink-0 items-center",
vertical ? "flex-col animate-marquee-vertical" : "flex-row animate-marquee",
pauseOnHover && "group-hover:[animation-play-state:paused]",
)}
>
{items.map((child, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: Marquee duplicates static child slots; item order is not mutated.
<div key={i} className="shrink-0">
{child}
</div>
))}
</div>
))}
</div>
);
}
@karaui not found? Add it to components.json
{
"registries": {
"@karaui": "https://karaui.kamisama-v0.my.id/r/{name}.json"
}
}@karaui is in shadcn's public registry directory, so the latest CLI needs no setup. On an older CLI, merge this into the existing file once and every @karaui/… install resolves.
API Reference
logosLogoCloudItem[]—variant?"grid" | "marquee""marquee" scrolls one infinite row; "grid" lays logos out and fades them in. Default "marquee".
marqueeeyebrow?ReactNode—heading?ReactNode—description?ReactNode—speed?numberSeconds for one marquee loop. Default 30.
30direction?"left" | "right"Marquee direction. Default "left".
leftpauseOnHover?booleanPause the marquee on hover or keyboard focus. Default true.
truecolorize?booleanLogos start muted (grayscale, dimmed) and take their own colors on hover. Default true.
truecolumns?3 | 4 | 5 | 6Grid columns at the widest container size. Default 4.
4className?string—itemClassName?stringClasses for each logo tile (sizing, padding).
—Keep in mind
Some components on this site are inspired by or recreated from existing work across the web. I'm not here to take credit; just to learn, experiment, and sometimes push things a bit further. If something looks familiar and I forgot to mention you, reach out and I'll fix that right away.
Updated