Fazekit

Code

"use client";
import * as React from "react";
import { AnimatePresence, motion, useInView, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export interface GridLogo {
  name: string;
  /** Logo artwork. Built-in logos use a coloured mark + currentColor wordmark so the greyscale hover works. */
  logo: React.ReactNode;
}

/* ---------- Invented brand marks ---------- */

function Brand({ name, color, width, mark, weight = 600, upper, spacing = -0.3 }: { name: string; color: string; width: number; mark: React.ReactNode; weight?: number; upper?: boolean; spacing?: number }) {
  return (
    <svg viewBox={`0 0 ${width} 32`} width={width} height="32" className="h-7 w-auto overflow-visible sm:h-8" role="img" aria-label={name}>
      <g fill={color} stroke={color}>
        {mark}
      </g>
      <text x="34" y="22.5" fill="currentColor" fontSize="19" fontWeight={weight} letterSpacing={spacing} style={{ fontFamily: "inherit" }}>
        {upper ? name.toUpperCase() : name}
      </text>
    </svg>
  );
}

const S = { strokeWidth: 0 } as const;

export const GRID_LOGOS: GridLogo[] = [
  { name: "Northwind", logo: <Brand name="Northwind" color="#0ea5e9" width={140} mark={<path {...S} d="M13 3 22 28l-9-5-9 5z" />} /> },
  {
    name: "Acme",
    logo: (
      <Brand name="acme" color="#f97316" width={96} weight={800} spacing={-1} mark={<><circle {...S} cx="10" cy="16" r="8" /><circle {...S} cx="19" cy="16" r="8" fillOpacity="0.5" /></>} />
    ),
  },
  {
    name: "Lumen",
    logo: (
      <Brand
        name="Lumen"
        color="#eab308"
        width={112}
        weight={500}
        upper
        spacing={1.5}
        mark={<><circle {...S} cx="14" cy="16" r="5" /><path fill="none" strokeWidth="2.2" strokeLinecap="round" d="M14 3v4M14 25v4M1 16h4M23 16h4M5 7l3 3M20 22l3 3M5 25l3-3M20 10l3-3" /></>}
      />
    ),
  },
  {
    name: "Orbit",
    logo: (
      <Brand name="orbit" color="#8b5cf6" width={96} weight={700} mark={<><circle {...S} cx="14" cy="16" r="5" /><ellipse cx="14" cy="16" rx="13" ry="6" fill="none" strokeWidth="2" transform="rotate(-25 14 16)" /></>} />
    ),
  },
  {
    name: "Vertex",
    logo: (
      <Brand name="Vertex" color="#10b981" width={112} weight={650} mark={<><path {...S} d="M14 4 26 11 14 18 2 11z" /><path fill="none" strokeWidth="2.4" strokeLinejoin="round" d="M2 17l12 7 12-7" /></>} />
    ),
  },
  {
    name: "Quanta",
    logo: (
      <Brand
        name="Quanta"
        color="#ec4899"
        width={118}
        mark={<><rect {...S} x="3" y="5" width="22" height="22" rx="6" /><rect x="9" y="11" width="10" height="10" rx="2" transform="rotate(45 14 16)" fill="var(--card)" stroke="none" /></>}
      />
    ),
  },
  { name: "Helix", logo: <Brand name="helix" color="#06b6d4" width={90} weight={700} mark={<path fill="none" strokeWidth="2.6" strokeLinecap="round" d="M3 9c5 0 5 14 11 14s6-14 11-14M3 23c5 0 5-14 11-14s6 14 11 14" />} /> },
  { name: "Nimbus", logo: <Brand name="Nimbus" color="#3b82f6" width={118} weight={550} mark={<path {...S} d="M8 24a6 6 0 0 1-.6-12A8 8 0 0 1 22.5 10 6 6 0 0 1 21 24z" />} /> },
  { name: "Polar", logo: <Brand name="POLAR" color="#6366f1" width={108} weight={800} spacing={2} mark={<path {...S} d="M14 3l3.2 9.6H27l-8 5.8 3 9.6-8-5.9-8 5.9 3-9.6-8-5.8h9.8z" />} /> },
  { name: "Kestrel", logo: <Brand name="Kestrel" color="#ef4444" width={120} mark={<path {...S} d="M2 10c7 0 11 3 12 9 2-8 7-13 13-14-3 4-4 9-5 13-2 6-7 8-11 7l3-4c-4 0-9-4-12-11z" />} /> },
  { name: "Cobalt", logo: <Brand name="Cobalt" color="#2563eb" width={112} weight={700} mark={<path {...S} d="M14 3l11 6.5v13L14 29 3 22.5v-13z" />} /> },
  { name: "Ember", logo: <Brand name="ember" color="#f43f5e" width={104} weight={700} spacing={-0.8} mark={<path {...S} d="M14 3c2 5 9 8 9 15a9 9 0 0 1-18 0c0-4 2-6 4-8 0 3 1 5 3 5 0-4 0-8 2-12z" />} /> },
  {
    name: "Fathom",
    logo: <Brand name="Fathom" color="#14b8a6" width={116} mark={<path fill="none" strokeWidth="2.6" strokeLinecap="round" d="M2 11c4-3 8 3 12 0s8 3 12 0M2 17c4-3 8 3 12 0s8 3 12 0M2 23c4-3 8 3 12 0s8 3 12 0" />} />,
  },
  { name: "Juniper", logo: <Brand name="Juniper" color="#22c55e" width={120} weight={550} mark={<path {...S} d="M5 27C5 13 13 5 26 4c0 13-7 21-19 21l9-10-11 12z" />} /> },
  {
    name: "Meridian",
    logo: (
      <Brand name="Meridian" color="#a855f7" width={136} weight={500} mark={<><circle cx="14" cy="16" r="11" fill="none" strokeWidth="2.2" /><ellipse cx="14" cy="16" rx="4.5" ry="11" fill="none" strokeWidth="2.2" /><path strokeWidth="2.2" d="M3 16h22" /></>} />
    ),
  },
  { name: "Tandem", logo: <Brand name="Tandem" color="#f59e0b" width={118} weight={700} mark={<><rect {...S} x="2" y="8" width="14" height="14" rx="3" /><rect {...S} x="12" y="12" width="14" height="14" rx="3" fillOpacity="0.55" /></>} /> },
];

/* ---------- Block ---------- */

export interface LogosGridFadeProps {
  eyebrow?: string;
  title?: string;
  description?: string;
  /** Pool of logos. The grid shows `slots` of them and cycles through the rest. */
  logos?: GridLogo[];
  /** Number of visible cells (4 or 8 work best). */
  slots?: number;
  /** Milliseconds between swaps (one cell changes per tick). */
  interval?: number;
  className?: string;
}

const ORDER_8 = [3, 6, 0, 5, 2, 7, 1, 4];
const EASE = [0.22, 1, 0.36, 1] as const;

export function LogosGridFade({
  eyebrow = "Customers",
  title = "Powering the teams behind products you use every day",
  description = "Hover a logo to see it in colour. The wall refreshes itself every few seconds.",
  logos = GRID_LOGOS,
  slots = 8,
  interval = 1800,
  className,
}: LogosGridFadeProps) {
  const count = Math.min(slots, logos.length);
  const order = React.useMemo(
    () => (count === 8 ? ORDER_8 : Array.from({ length: count }, (_, i) => (i * 3) % count)),
    [count],
  );
  const [state, setState] = React.useState(() => ({
    shown: Array.from({ length: count }, (_, i) => i),
    cursor: count,
    tick: 0,
  }));
  const [paused, setPaused] = React.useState(false);
  const ref = React.useRef<HTMLDivElement>(null);
  const inView = useInView(ref, { amount: 0.3 });
  const reduce = useReducedMotion();

  React.useEffect(() => {
    if (!inView || paused || logos.length <= count) return;
    const id = setInterval(() => {
      setState((s) => {
        const slot = order[s.tick % order.length];
        let c = s.cursor;
        let guard = 0;
        while (s.shown.includes(c % logos.length) && guard++ < logos.length) c++;
        const shown = [...s.shown];
        shown[slot] = c % logos.length;
        return { shown, cursor: c + 1, tick: s.tick + 1 };
      });
    }, interval);
    return () => clearInterval(id);
  }, [inView, paused, interval, logos.length, count, order]);

  return (
    <section className={cn("relative w-full overflow-x-clip bg-background py-16 sm:py-20", className)}>
      <div className="mx-auto max-w-6xl px-4 sm:px-6">
        <div className="mx-auto max-w-2xl text-center">
          <p className="text-sm font-medium text-primary">{eyebrow}</p>
          <h2 className="mt-2 text-balance text-2xl font-semibold tracking-tight text-foreground sm:text-3xl">{title}</h2>
          <p className="mt-3 text-pretty text-muted-foreground">{description}</p>
        </div>

        <div
          ref={ref}
          onPointerEnter={() => setPaused(true)}
          onPointerLeave={() => setPaused(false)}
          className="mt-12 grid grid-cols-2 gap-px overflow-hidden rounded-3xl border bg-border sm:grid-cols-4"
        >
          {state.shown.slice(0, count).map((logoIdx, slot) => {
            const item = logos[logoIdx];
            return (
              <div key={slot} className="group relative flex h-24 items-center justify-center overflow-hidden bg-card sm:h-28">
                <div
                  aria-hidden
                  className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_center,var(--accent),transparent_70%)] opacity-0 transition-opacity duration-300 group-hover:opacity-100"
                />
                <AnimatePresence mode="popLayout" initial={false}>
                  <motion.div
                    key={item.name}
                    initial={reduce ? { opacity: 0 } : { opacity: 0, y: 18, filter: "blur(6px)" }}
                    animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
                    exit={reduce ? { opacity: 0 } : { opacity: 0, y: -18, filter: "blur(6px)" }}
                    transition={{ duration: 0.6, ease: EASE }}
                    className="relative"
                  >
                    <div className="text-foreground/70 opacity-80 grayscale transition-[filter,opacity,color] duration-300 group-hover:text-foreground group-hover:opacity-100 group-hover:grayscale-0">
                      {item.logo}
                    </div>
                  </motion.div>
                </AnimatePresence>
              </div>
            );
          })}
        </div>
      </div>
    </section>
  );
}

More in Logo Clouds

View all →