A logo wall that shows many customers in the space of four. The section has a heading, a line of copy and a row of four tiles divided by hairlines. Each tile holds a share of the twelve logos and swaps to its next one every couple of seconds: the old logo drifts up and blurs out, the new one rises in and sharpens with a small spring. The tiles change one after another from left to right, so the eye is led along the row. On phones the tiles form a two by two grid and the logos shrink to fit.
// Adapted from Cult UI (https://www.cult-ui.com/docs/components/logo-carousel)
// Copyright (c) 2023 Jordan-Gilliam. MIT License: https://we.inc/components/licenses
import { useEffect, useState, type ReactNode } from "react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
/** Invented customer wordmarks: a small drawn mark and a name. Replace with your own. */
const logos: { name: string; mark: ReactNode; style: string }[] = [
{ name: "Northwind", style: "font-semibold tracking-tight", mark: <path d="M4 20 12 4l8 16-8-5z" fill="currentColor" /> },
{ name: "halcyon", style: "font-light tracking-wide lowercase", mark: <><circle cx="9" cy="12" r="6" fill="none" stroke="currentColor" strokeWidth="2.2" /><circle cx="15" cy="12" r="6" fill="none" stroke="currentColor" strokeWidth="2.2" /></> },
{ name: "BRICKYARD", style: "font-black tracking-tighter", mark: <><rect x="3" y="5" width="8" height="6" fill="currentColor" /><rect x="13" y="5" width="8" height="6" fill="currentColor" /><rect x="8" y="13" width="8" height="6" fill="currentColor" /></> },
{ name: "Juniper & Co", style: "font-serif italic", mark: <path d="M12 3c5 3 7 8 0 18C5 11 7 6 12 3z" fill="currentColor" /> },
{ name: "Kestrel", style: "font-bold tracking-tight", mark: <path d="M3 8l9 4 9-4-9 12z" fill="currentColor" /> },
{ name: "sundial", style: "font-medium lowercase tracking-tight", mark: <><circle cx="12" cy="12" r="4" fill="currentColor" /><path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /></> },
{ name: "OTTERLINE", style: "font-semibold tracking-[0.2em] text-[0.82em]", mark: <path d="M2 14c3-6 6-6 9 0s6 6 11 0" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" /> },
{ name: "Maple Row", style: "font-serif font-semibold", mark: <><rect x="4" y="4" width="16" height="16" rx="4" fill="currentColor" /><path d="M8 16V8l4 5 4-5v8" fill="none" stroke="#fff" strokeWidth="2" strokeLinejoin="round" /></> },
{ name: "Quarto", style: "font-extrabold tracking-tight", mark: <><path d="M4 4h7v7H4z" fill="currentColor" /><path d="M13 13h7v7h-7z" fill="currentColor" /><path d="M13 4h7v7h-7zM4 13h7v7H4z" fill="currentColor" opacity="0.35" /></> },
{ name: "tidewater", style: "font-normal lowercase tracking-wider", mark: <path d="M12 3c4 5 6 8 6 11a6 6 0 0 1-12 0c0-3 2-6 6-11z" fill="currentColor" /> },
{ name: "Fablehaus", style: "font-bold", mark: <path d="M12 3l9 7v11h-6v-7H9v7H3V10z" fill="currentColor" /> },
{ name: "VOLT&VINE", style: "font-black italic tracking-tight", mark: <path d="M13 2 5 13h6l-1 9 9-12h-6z" fill="currentColor" /> },
];
const SLOTS = 4;
const SHOW_MS = 2400; // how long each logo stays
const STAGGER_MS = 350; // delay between neighbouring slots
/** Deal the logos out so every slot has its own equal share. */
const sets = Array.from({ length: SLOTS }, (_, slot) => logos.filter((_, i) => i % SLOTS === slot));
function Slot({ index, time }: { index: number; time: number }) {
const reduce = useReducedMotion();
const set = sets[index];
const current = Math.floor(Math.max(0, time - index * STAGGER_MS) / SHOW_MS) % set.length;
const logo = set[current];
return (
<motion.li
initial={reduce ? false : { opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1, duration: 0.5, ease: "easeOut" }}
className="relative h-24 overflow-hidden bg-white sm:h-32"
>
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={logo.name}
className="absolute inset-0 flex items-center justify-center gap-2.5 px-3 text-slate-800"
initial={reduce ? { opacity: 0 } : { y: "18%", opacity: 0, filter: "blur(8px)" }}
animate={{ y: "0%", opacity: 1, filter: "blur(0px)", transition: { type: "spring", stiffness: 300, damping: 20 } }}
exit={reduce ? { opacity: 0 } : { y: "-22%", opacity: 0, filter: "blur(6px)", transition: { duration: 0.3, ease: "easeIn" } }}
>
<svg viewBox="0 0 24 24" className="h-6 w-6 shrink-0 sm:h-7 sm:w-7" aria-hidden="true">
{logo.mark}
</svg>
<span className={`whitespace-nowrap text-lg sm:text-2xl ${logo.style}`}>{logo.name}</span>
</motion.div>
</AnimatePresence>
</motion.li>
);
}
export default function LogoWallThatCyclesLogosWithBlurSwap() {
const reduce = useReducedMotion();
const [time, setTime] = useState(0);
useEffect(() => {
if (reduce) return;
const started = performance.now();
const t = window.setInterval(() => !document.hidden && setTime(performance.now() - started + 1300), 150);
return () => window.clearInterval(t);
}, [reduce]);
return (
<section className="bg-slate-50 px-4 py-14 sm:px-6 sm:py-20">
<div className="mx-auto max-w-5xl">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl">The books behind 2,400 small shops</h2>
<p className="mt-3 text-base text-slate-600">From one person studios to forty seat restaurants, they close their month in Ledgerleaf.</p>
</div>
<ul aria-label={`Customers include ${logos.map((l) => l.name).join(", ")}`} className="mt-10 grid grid-cols-2 gap-px overflow-hidden rounded-2xl bg-slate-200 ring-1 ring-slate-200 md:grid-cols-4">
{sets.map((_, i) => (
<Slot key={i} index={i} time={time} />
))}
</ul>
<p className="mt-6 text-center text-sm text-slate-600">
<a href="#" className="rounded font-semibold text-slate-900 underline underline-offset-4 outline-none hover:text-slate-700 focus-visible:ring-2 focus-visible:ring-slate-900 focus-visible:ring-offset-2">
Read their stories
</a>{" "}
or start a free month yourself.
</p>
</div>
</section>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/logo-wall-that-cycles-logos-with-blur-swap.json
A marquee scrolls a long strip sideways without stopping. Here the layout is fixed and each tile changes its content in place, which is calmer, never clips a logo at the edge and takes the same space on every screen.
They are dealt out in order: with four tiles, logos 1, 5 and 9 go to the first tile, 2, 6 and 10 to the second, and so on. A logo therefore never appears in two tiles at once.
A single elapsed time value is passed to all tiles, and each works out which logo to show from that time minus its own offset. The tiles can never drift out of step the way separate timers would.
It still works. Tiles with fewer logos simply loop a little sooner. For the tidiest result use a multiple of the tile count.
Yes. Replace the SVG and span inside `Slot` with an `<img>` that has an `alt` text, and keep it inside the animated wrapper.
Adapted from Logo Carousel in Cult UI, used under its open-source licence. Licences and credits
Related: Logo Cloud with Drawn Wordmarks, Dual Row Logo Marquee with Edge Fade, Centered Hero with Two Buttons and Logo Row.
More marquees or all free UI components. Start for free to have a full site built around it.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.