Logo wall

The "trusted by" row - one optical height for logos that arrive at a dozen sizes, colour drained so the palettes stop competing, and text logos beside the images.

A URL is enough for most logos:

<LogoMarquee logos={["/logos/openai.svg", "/logos/stripe.svg"]} speed={40} />

Some of them are not images, though - Bloomberg, NASA and Harvard are set in type - and some need a name. The three forms mix in one list:

<LogoMarquee
    logos={[
        "/logos/openai.svg",                                  // a URL is enough
        { kind: "img",  src: stripeSvgUrl, alt: "Stripe" },   // unless it needs an alt
        { kind: "node", node: <span>NASA</span> }             // or is set in type
    ]}
    speed={40}
/>

A bare URL carries no alt. One guessed from the file name would be a fabricated one, and a wrong alt is worse than an empty one - so pass the object form for a mark that needs a name, and label names the row itself.

Customize

Installation

Adds the package and prints the import to use. Append --copy to write the source into your project instead.

enigma add logo-marquee

Installs with your project's own package manager - npm, pnpm, yarn or bun - read from its packageManager field or its lockfile.

The two things that make a wall look designed

One optical height. Logos arrive at a dozen different sizes and aspect ratios, and a row that drops them in as they come reads as random. Every image is scaled to logoHeight and never stretched - the aspect ratio of a logo is the brand. One that still reads small takes its own height.

Drained colour. Twelve brand palettes side by side compete with each other and pull the eye off the copy beside them, so tone="mono" greys them out and gives the colour back to whichever one you point at. tone="brand" leaves them alone.

Speed is pixels per second

Not seconds per loop. A lap is as long as its content, so a duration makes the speed content / duration and the row runs faster every time you add a logo - measured on one rail at 45, 67 and 87 px/s with 10, 15 and 20 items. Take a speed and the duration follows.

That is also why the number of copies is measured from the DOM rather than fixed at two: two copies leave a gap the moment the content is narrower than the lane, which is the common case for a short wall on a wide screen.

Props

Default
logos A URL string, { kind: "img", src, alt?, height? }, or { kind: "node", node, key? }
speed 40 Pixels per second
logoHeight 28 The shared height every image is scaled to
gap 56 Space between logos, in px
tone "mono" "brand" keeps the colours
fade true Fades both ends so a logo leaves instead of being sliced
hover "off" "pause", a multiplier, or { speed }
reverse false
label Names the row for a screen reader, e.g. “Trusted by”

Only the first copy is read aloud; the rest carry aria-hidden, because a screen reader announcing the same twelve brands three times is the bug that hides in every hand-rolled version. An image’s alt is empty unless you give it one, for the same reason.