> ## Documentation Index
> Fetch the complete documentation index at: https://www.logo.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Build an animated logo wall

> Show more customers and integrations in less space. Build an animated logo wall where every logo loads from a Logo.dev image URL.

export const LogoGridDemo = () => {
  const DEMO_TOKEN = "live_6a1a28fd-6420-4492-aeb0-b297461d9de2";
  const DOMAINS = ["stripe.com", "shopify.com", "github.com", "figma.com", "notion.so", "linear.app", "vercel.com", "slack.com", "zoom.us", "spotify.com", "airbnb.com", "netflix.com", "amazon.com", "google.com", "microsoft.com", "apple.com", "dropbox.com", "atlassian.com", "gitlab.com", "cloudflare.com", "datadoghq.com", "twilio.com", "hubspot.com", "salesforce.com", "intercom.com", "zendesk.com", "mailchimp.com", "openai.com", "anthropic.com", "nvidia.com", "adobe.com", "canva.com", "framer.com", "webflow.com", "discord.com", "reddit.com", "pinterest.com", "paypal.com", "coinbase.com", "robinhood.com", "doordash.com", "uber.com", "asana.com", "segment.com", "x.com"];
  const TILE_COUNT = 15;
  const DECK_SIZE = 3;
  const COLUMNS = 5;
  const STEP_MS = 3000;
  const REVEAL_MS = 400;
  const COLUMN_DELAY_MS = 120;
  const TRAVEL_PX = 40;
  const EASE = "cubic-bezier(0.17, 0.17, 0.3, 1)";
  const decks = useMemo(() => {
    const out = [];
    for (let i = 0; i < TILE_COUNT; i++) {
      out.push(DOMAINS.slice(i * DECK_SIZE, i * DECK_SIZE + DECK_SIZE));
    }
    return out;
  }, []);
  const [active, setActive] = useState(0);
  useEffect(() => {
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      return;
    }
    let inView = true;
    const gridEl = document.getElementById("logo-grid-demo");
    const observer = gridEl && ("IntersectionObserver" in window) ? new IntersectionObserver(([entry]) => {
      inView = entry.isIntersecting;
    }) : null;
    observer?.observe(gridEl);
    const id = setInterval(() => {
      if (inView) {
        setActive(v => (v + 1) % DECK_SIZE);
      }
    }, STEP_MS);
    return () => {
      clearInterval(id);
      observer?.disconnect();
    };
  }, []);
  const layerStyle = (index, columnIndex) => {
    const delay = (columnIndex + 1) * COLUMN_DELAY_MS;
    const transition = `opacity ${REVEAL_MS}ms ${EASE} ${delay}ms, translate ${REVEAL_MS}ms ${EASE} ${delay}ms`;
    if (index === active) {
      return {
        opacity: 1,
        translate: "0 0",
        transition
      };
    }
    if (index === (active - 1 + DECK_SIZE) % DECK_SIZE) {
      return {
        opacity: 0,
        translate: `0 -${TRAVEL_PX}px`,
        transition
      };
    }
    return {
      opacity: 0,
      translate: `0 ${TRAVEL_PX}px`,
      transition
    };
  };
  const logoSrc = domain => `https://img.logo.dev/${domain}?token=${DEMO_TOKEN}&format=webp&retina=true&size=128`;
  return <div className="not-prose my-8 overflow-hidden rounded-2xl border border-zinc-950/10 bg-gradient-to-br from-zinc-50 to-white p-6 shadow-sm sm:p-8 dark:border-white/10 dark:from-zinc-900 dark:to-zinc-950">
      <div aria-label="A grid of company logos that swap over time, served from the Logo.dev image API" className="grid grid-cols-3 gap-3 sm:grid-cols-5 sm:gap-4" id="logo-grid-demo" role="img">
        {decks.map((deck, tileIndex) => <div className="relative flex aspect-square items-center justify-center" key={`tile-${tileIndex}`}>
            {deck.map((domain, layerIndex) => <span aria-hidden="true" className="absolute inset-0 flex items-center justify-center" key={domain} style={layerStyle(layerIndex, tileIndex % COLUMNS)}>
                <img alt="" className="h-[52px] w-[52px] object-contain" height="52" loading="lazy" src={logoSrc(domain)} width="52" />
              </span>)}
          </div>)}
      </div>
    </div>;
};

A logo wall is how a landing page shows traction: customers, integrations, supported companies. Animating it fits three times the brands into the same space and adds motion without a video.

A visitor scrolling past sees a calm wave pass across the grid as every tile swaps to its next logo, column by column. You feed it a list of domains, and every logo is a [Logo API](/docs/logo-images/introduction) image URL, so there are no logo files to keep current.

## Demo

<LogoGridDemo />

Every logo above loads live from `img.logo.dev`. The wall advances every few seconds, sweeping left to right one column at a time.

## Build it with AI

<Prompt description="Want this wall in your app? This prompt gives your AI coding tool everything it needs to build it in your framework." actions={["copy", "cursor"]}>
  Build an animated logo wall using Logo.dev.

  * Render a grid (default 5 columns by 3 rows) where each tile stacks a deck of 3 logos as always-mounted layers.
  * Logos are plain image URLs: [https://img.logo.dev/:domain?token=LOGO\_DEV\_PUBLISHABLE\_KEY\&format=webp\&retina=true\&size=128](https://img.logo.dev/:domain?token=LOGO_DEV_PUBLISHABLE_KEY\&format=webp\&retina=true\&size=128)
  * Keep every layer in one of three positions: the current logo at translate 0 and full opacity, the previous logo parked 40px above at opacity 0, and the next logo waiting 40px below at opacity 0.
  * Advance all tiles together every 3 seconds, with a 120ms-per-column transition delay so the swap sweeps left to right as a wave. Transitions run 400ms with cubic-bezier(0.17, 0.17, 0.3, 1). Animate only opacity and translate.
  * Pause the interval while the grid is off-screen (IntersectionObserver) and render a static grid under prefers-reduced-motion.
  * Use a Logo.dev publishable key (safe in client code), never a secret key.
  * Reference implementation: [https://www.logo.dev/docs/use-cases/animated-logo-wall](https://www.logo.dev/docs/use-cases/animated-logo-wall)
</Prompt>

## Code

This example is in React, but you can get the same result in any frontend framework: the wall is just image URLs and CSS transitions. Paste `LogoGrid.tsx`, then render it as the Usage tab shows. A 5 × 3 grid cycling 3 logos per tile needs 45 domains.

<CodeGroup>
  ```tsx LogoGrid.tsx expandable theme={null}
  import { useEffect, useMemo, useRef, useState } from "react";
  import type { CSSProperties } from "react";

  type LogoGridProps = {
    /** Your Logo.dev publishable key (pk_… / live_…). */
    token: string;
    /** Domains to show. Provide columns × rows × deckSize of them. */
    domains: string[];
    columns?: number;
    rows?: number;
    /** How many logos each tile cycles through. */
    deckSize?: number;
  };

  // One global tick advances every tile together; the per-column transition
  // delay turns the shared swap into a left-to-right wave.
  const STEP_MS = 3000;
  const REVEAL_MS = 400;
  const COLUMN_DELAY_MS = 120;
  const TRAVEL_PX = 40;
  const EASE = "cubic-bezier(0.17, 0.17, 0.3, 1)";

  function useReducedMotion() {
    const [reduced, setReduced] = useState(false);
    useEffect(() => {
      const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
      const update = () => setReduced(mq.matches);
      update();
      mq.addEventListener("change", update);
      return () => mq.removeEventListener("change", update);
    }, []);
    return reduced;
  }

  export function LogoGrid({
    token,
    domains,
    columns = 5,
    rows = 3,
    deckSize = 3,
  }: LogoGridProps) {
    const ref = useRef<HTMLDivElement>(null);
    const [inView, setInView] = useState(false);
    const reduced = useReducedMotion();
    const [active, setActive] = useState(0);

    const decks = useMemo(() => {
      const tiles = columns * rows;
      return Array.from({ length: tiles }, (_, i) =>
        domains.slice(i * deckSize, i * deckSize + deckSize)
      );
    }, [domains, columns, rows, deckSize]);

    // Only animate while the grid is on screen.
    useEffect(() => {
      const el = ref.current;
      if (!el) {
        return;
      }
      const observer = new IntersectionObserver(([entry]) =>
        setInView(entry.isIntersecting)
      );
      observer.observe(el);
      return () => observer.disconnect();
    }, []);

    const running = inView && !reduced;

    useEffect(() => {
      if (!running) {
        return;
      }
      const id = setInterval(() => {
        setActive((v) => (v + 1) % deckSize);
      }, STEP_MS);
      return () => clearInterval(id);
    }, [running, deckSize]);

    // Every logo in a deck stays mounted in one of three positions: the current
    // logo in place, the previous one parked above, and the next waiting below.
    // Because the incoming layer is already in its start position, the swap is a
    // plain style change and the transition fires with no extra bookkeeping.
    const layerStyle = (index: number, columnIndex: number): CSSProperties => {
      const delay = (columnIndex + 1) * COLUMN_DELAY_MS;
      const transition = `opacity ${REVEAL_MS}ms ${EASE} ${delay}ms, translate ${REVEAL_MS}ms ${EASE} ${delay}ms`;
      if (index === active) {
        return { opacity: 1, translate: "0 0", transition };
      }
      if (index === (active - 1 + deckSize) % deckSize) {
        return { opacity: 0, translate: `0 -${TRAVEL_PX}px`, transition };
      }
      return { opacity: 0, translate: `0 ${TRAVEL_PX}px`, transition };
    };

    const src = (domain: string) =>
      `https://img.logo.dev/${domain}?token=${token}&format=webp&retina=true&size=128`;

    return (
      <div
        aria-label="Company logos served from the Logo.dev image API"
        className="grid gap-3 sm:gap-4 overflow-hidden"
        ref={ref}
        role="img"
        style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}
      >
        {decks.map((deck, tileIndex) => (
          <div
            className="relative flex aspect-square items-center justify-center"
            key={`tile-${tileIndex}`}
          >
            {deck.map((domain, layerIndex) => (
              <span
                aria-hidden="true"
                className="absolute inset-0 flex items-center justify-center"
                key={domain}
                style={layerStyle(layerIndex, tileIndex % columns)}
              >
                <img
                  alt=""
                  className="h-[52px] w-[52px] object-contain"
                  height={52}
                  loading="lazy"
                  src={src(domain)}
                  width={52}
                />
              </span>
            ))}
          </div>
        ))}
      </div>
    );
  }
  ```

  ```tsx Usage theme={null}
  import { LogoGrid } from "./LogoGrid";

  const DOMAINS = [
    "stripe.com", "shopify.com", "github.com", "figma.com", "notion.so",
    "linear.app", "vercel.com", "slack.com", "zoom.us", "spotify.com",
    "airbnb.com", "netflix.com", "amazon.com", "google.com", "microsoft.com",
    "apple.com", "dropbox.com", "atlassian.com", "gitlab.com", "cloudflare.com",
    "datadoghq.com", "twilio.com", "hubspot.com", "salesforce.com", "intercom.com",
    "zendesk.com", "mailchimp.com", "openai.com", "anthropic.com", "nvidia.com",
    "adobe.com", "canva.com", "framer.com", "webflow.com", "discord.com",
    "reddit.com", "pinterest.com", "paypal.com", "coinbase.com", "robinhood.com",
    "doordash.com", "uber.com", "asana.com", "segment.com", "x.com",
  ];

  export function Customers() {
    return <LogoGrid token="LOGO_DEV_PUBLISHABLE_KEY" domains={DOMAINS} />;
  }
  ```
</CodeGroup>

<Note>
  Your [publishable key](/docs/platform/api-keys) is built for client-side code, so
  you can ship it in the browser as-is.
</Note>

## How it works

* **Every logo is one image URL.** `img.logo.dev/:domain` returns the company's logo, and query parameters handle the rest: `size` for dimensions, `retina` for sharp rendering, `format=webp` for weight. See all [image parameters](/docs/logo-images/introduction#parameters).
* **Unknown domains still render.** When Logo.dev doesn't have a logo, it returns a generated monogram instead of a broken image, so the wall works with any domain list you give it. See [fallback images](/docs/logo-images/introduction#fallback-images).
* **The CDN does the heavy lifting.** Logos load like any other cached image, so swapping them live costs nothing beyond a normal image request.
* **The motion is plain CSS.** Every logo in a tile's deck stays mounted as a stacked layer, and one shared tick sweeps the whole wall left to right. The wall pauses off-screen and holds still for visitors who prefer reduced motion.

## Make it your own

* **Restyle the logos.** Add `&theme=dark` for dark backgrounds or `&greyscale=true` for a muted, uniform wall. See all [image parameters](/docs/logo-images/introduction#parameters).
* **Change the shape.** Set `columns`, `rows`, and `deckSize`, and keep `domains.length` at `columns × rows × deckSize`.
* **Make it static.** Render one logo per tile and skip the timers for a plain logo cloud, or use the [customer logo wall](/docs/use-cases/customer-logo-wall) for wide wordmark lockups.

## Next steps

<CardGroup cols={2}>
  <Card title="Logo API" icon="image" href="/docs/logo-images/introduction">
    Every parameter for the image URL: size, format, theme, greyscale, and fallbacks.
  </Card>

  <Card title="Search API" icon="magnifying-glass" href="/docs/brand-search/introduction">
    Have company names instead of domains? Resolve them first.
  </Card>
</CardGroup>
