Logo Marquee — Free HTML CSS JS Infinite Scroll Snippet

Logo Marquee · Animations · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Seamless CSS loop
A duplicated set and translateX(-50%) loop invisibly.
Count-based pacing
Duration scales with the number of logos.
Grayscale-to-color hover
Logos brighten when pointed at.
Pure-CSS pause on hover
animation-play-state stops the strip to read.
Edge fade mask
Horizontal gradient mask dissolves the ends.
Inline SVG marks
Crisp, image-free, no extra requests.
Data-driven
One BRANDS array builds every logo.
Compositor-friendly
No per-frame JavaScript; smooth on mobile.

About this UI Snippet

Logo Marquee — Seamless Infinite Brand Logo Scroller

Screenshot of the Logo Marquee snippet rendered live

The logo marquee is the "trusted by" strip on nearly every B2B landing page: a horizontal row of customer or partner logos that scrolls continuously and loops forever. This snippet builds it in plain HTML, CSS, and a few lines of vanilla JavaScript, with a seamless loop, grayscale-to-color hover, edge fades, and pause-on-hover — and uses lightweight inline SVG marks so there are no image requests.

A seamless CSS loop

The whole scroll is driven by one CSS keyframe: lmScroll translates the track to translateX(-50%) over its duration. The reason -50% produces an endless loop is duplication — JavaScript renders the brand set twice (set + set), so the track is exactly two identical halves. When the animation reaches -50%, the first half has scrolled completely off and the second half sits precisely where the first began, so the keyframe restart is invisible. No JavaScript runs per frame; the browser's compositor handles the motion efficiently.

Pacing tied to the logo count

Rather than hard-coding a duration, JavaScript sets animationDuration to BRANDS.length * 3.4 seconds. This keeps the perceived speed steady no matter how many logos you add — twice as many logos take twice as long, so each logo spends the same amount of time on screen. Add or remove brands and the pace stays natural without manual retuning.

Grayscale to color on hover

Each logo renders desaturated and slightly dimmed via filter: grayscale(1) and reduced opacity, which is the conventional "logo wall" treatment that keeps the strip calm and uniform. On hover, the filter drops to grayscale(0) and full opacity so the pointed-at brand pops into color — a small interaction that rewards exploration. The transition on both filter and color makes the shift smooth.

Pause on hover

The track's animation is paused whenever the marquee is hovered, using the pure-CSS rule .lm-marquee:hover .lm-track { animation-play-state: paused }. That lets a visitor stop the strip to read a specific logo without any JavaScript, then it resumes seamlessly when the cursor leaves.

Edge fades with a mask

To avoid logos hard-cutting at the container edges, a horizontal mask-image gradient fades the leftmost and rightmost 10% to transparent, so brands dissolve in and out rather than popping. It's a compositing effect with no overlay elements.

Data-driven, image-free logos

All brands live in a BRANDS array of name, inline SVG path, and color, and a logoHTML function builds each link. Using inline SVG keeps the marquee crisp on any display and avoids the layout shift and extra requests of bitmap logos. To use your real customers, replace the array entries — drop in their SVG marks or swap the SVG for an <img> — and the loop, pacing, and fades keep working.

Customizing it

Change the 3.4 multiplier for a faster or slower crawl, widen the gap between logos, adjust the mask percentages for a longer fade, or remove the grayscale treatment for full-color logos. To scroll the other direction, animate to translateX(50%) from a -50% start, or flip the row with flex-direction: row-reverse. Pair it with a testimonial wall or place it under a feature tabs showcase for a complete social-proof section.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA row of brand logos scrolls smoothly and loops forever.
  2. 2
    Hover a logoIt brightens from grayscale to full color.
  3. 3
    Hover the stripThe whole marquee pauses so you can read a brand.
  4. 4
    Note the edge fadesLogos dissolve in and out at the left and right.
  5. 5
    Swap in your brandsReplace the BRANDS array with real logos or images.
  6. 6
    Tune the paceAdjust the duration multiplier and logo gap.

Real-world uses

Common Use Cases

Trusted-by strips
Above the fold near a feature tabs showcase.
Social proof sections
Pair with a testimonial wall.
Partner pages
An animated take on a logo cloud grid.
Agency sites
Show client brands beneath an agency hero.
Conference microsites
Scroll sponsor logos continuously.
Marquee learning
A reference for seamless CSS-only infinite scroll.

Got questions?

Frequently Asked Questions

The track contains the logo set rendered twice, so it is two identical halves. Animating to translateX(-50%) scrolls exactly the first half off-screen, at which point the second half sits where the first started. The keyframe then restarts at 0, which looks identical, so the loop is seamless with no JavaScript per frame.

JavaScript sets animationDuration to the logo count times 3.4 seconds. Because both the distance and the duration scale with the number of logos, each logo spends the same time on screen regardless of how many there are, so the crawl speed feels identical whether you show six brands or sixteen.

A pure-CSS rule pauses the animation on hover: .lm-marquee:hover .lm-track sets animation-play-state to paused. No JavaScript is needed — moving the cursor over the strip freezes it, and leaving resumes the scroll exactly where it stopped.

Yes. The BRANDS array holds each logo's name, inline SVG, and color. Replace those entries with your customers' SVG marks, or swap the inline SVG for an <img> tag pointing at logo files. Inline SVG keeps the strip sharp on high-DPI screens and avoids extra network requests and layout shift.

Render the doubled logo list from your data and set the animation duration via an inline style based on the array length. The CSS keyframe and hover pause work as-is. In Tailwind, define the scroll keyframe in your config, apply animate-[scroll_26s_linear_infinite], and use the group-hover utility to pause on hover with a masked container for the edge fades.

Logo Marquee — Export as HTML, React, Vue, Angular & Tailwind

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Logo Marquee</title>
  <style>
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#0b0f1a;color:#e2e8f0;display:flex;align-items:center;min-height:100vh}
    
    .lm-section{width:100%;padding:40px 0}
    .lm-eyebrow{text-align:center;font-size:12px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:#64748b;margin-bottom:26px}
    
    .lm-marquee{overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 10%,#000 90%,transparent);mask-image:linear-gradient(90deg,transparent,#000 10%,#000 90%,transparent)}
    .lm-track{display:flex;align-items:center;gap:56px;width:max-content;animation:lmScroll 26s linear infinite}
    .lm-marquee:hover .lm-track{animation-play-state:paused}
    @keyframes lmScroll{to{transform:translateX(-50%)}}
    
    .lm-logo{display:flex;align-items:center;gap:10px;color:#94a3b8;flex-shrink:0;transition:color .25s,filter .25s;filter:grayscale(1);opacity:.85}
    .lm-logo:hover{color:#fff;filter:grayscale(0);opacity:1}
    .lm-logo svg{width:26px;height:26px}
    .lm-name{font-size:19px;font-weight:800;letter-spacing:-.02em}
  </style>
</head>
<body>
  <section class="lm-section">
    <p class="lm-eyebrow">Trusted by teams at</p>
    <div class="lm-marquee" id="lmMarquee">
      <div class="lm-track" id="lmTrack"></div>
    </div>
  </section>
  <script>
    var BRANDS = [
      { name: 'Nimbus',   icon: '<circle cx="12" cy="12" r="9"/><path d="M7 13a5 5 0 0 1 10 0" fill="none" stroke="currentColor" stroke-width="2"/>', c: '#60a5fa' },
      { name: 'Vertex',   icon: '<path d="M12 3 21 19H3z"/>', c: '#a78bfa' },
      { name: 'Pulse',    icon: '<path d="M2 12h5l2-6 4 12 2-6h7" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round"/>', c: '#f472b6' },
      { name: 'Quanta',   icon: '<rect x="4" y="4" width="16" height="16" rx="4"/><circle cx="12" cy="12" r="3" fill="#0b0f1a"/>', c: '#34d399' },
      { name: 'Lumen',    icon: '<path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2"/><circle cx="12" cy="12" r="4"/>', c: '#fbbf24' },
      { name: 'Forge',    icon: '<path d="M4 14h10l4-8 2 4-3 6H4z"/>', c: '#fb7185' },
      { name: 'Atlas',    icon: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18" fill="none" stroke="#0b0f1a" stroke-width="1.6"/>', c: '#22d3ee' },
      { name: 'Drift',    icon: '<path d="M3 16c4 0 4-8 9-8s5 8 9 8" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>', c: '#c084fc' }
    ];
    
    function logoHTML(b) {
      return '<a href="#" class="lm-logo" aria-label="' + b.name + '">' +
        '<svg viewBox="0 0 24 24" fill="' + b.c + '">' + b.icon + '</svg>' +
        '<span class="lm-name">' + b.name + '</span></a>';
    }
    
    // Render the brand set twice so the -50% translate loops seamlessly.
    var track = document.getElementById('lmTrack');
    var set = BRANDS.map(logoHTML).join('');
    track.innerHTML = set + set;
    
    // Match animation duration to the number of logos for a steady pace.
    track.style.animationDuration = (BRANDS.length * 3.4) + 's';
  </script>
</body>
</html>
Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Logo Marquee</title>
  <!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    @keyframes lmScroll{to{transform:translateX(-50%)}}

    * {
      box-sizing:border-box;margin:0;padding:0
    }

    body {
      font-family:system-ui,-apple-system,sans-serif;background:#0b0f1a;color:#e2e8f0;display:flex;align-items:center;min-height:100vh
    }

    .lm-marquee:hover .lm-track {
      animation-play-state:paused
    }

    .lm-logo {
      display:flex;align-items:center;gap:10px;color:#94a3b8;flex-shrink:0;transition:color .25s,filter .25s;filter:grayscale(1);opacity:.85
    }

    .lm-logo:hover {
      color:#fff;filter:grayscale(0);opacity:1
    }

    .lm-logo svg {
      width:26px;height:26px
    }

    .lm-name {
      font-size:19px;font-weight:800;letter-spacing:-.02em
    }
  </style>
</head>
<body>
  <section class="w-full py-10 px-0">
    <p class="text-center text-xs font-semibold tracking-[.14em] uppercase text-[#64748b] mb-[26px]">Trusted by teams at</p>
    <div class="lm-marquee overflow-hidden [-webkit-mask-image:linear-gradient(90deg,transparent,#000_10%,#000_90%,transparent)] [mask-image:linear-gradient(90deg,transparent,#000_10%,#000_90%,transparent)]" id="lmMarquee">
      <div class="lm-track flex items-center gap-14 w-max [animation:lmScroll_26s_linear_infinite]" id="lmTrack"></div>
    </div>
  </section>
  <script>
    var BRANDS = [
      { name: 'Nimbus',   icon: '<circle cx="12" cy="12" r="9"/><path d="M7 13a5 5 0 0 1 10 0" fill="none" stroke="currentColor" stroke-width="2"/>', c: '#60a5fa' },
      { name: 'Vertex',   icon: '<path d="M12 3 21 19H3z"/>', c: '#a78bfa' },
      { name: 'Pulse',    icon: '<path d="M2 12h5l2-6 4 12 2-6h7" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round"/>', c: '#f472b6' },
      { name: 'Quanta',   icon: '<rect x="4" y="4" width="16" height="16" rx="4"/><circle cx="12" cy="12" r="3" fill="#0b0f1a"/>', c: '#34d399' },
      { name: 'Lumen',    icon: '<path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2"/><circle cx="12" cy="12" r="4"/>', c: '#fbbf24' },
      { name: 'Forge',    icon: '<path d="M4 14h10l4-8 2 4-3 6H4z"/>', c: '#fb7185' },
      { name: 'Atlas',    icon: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18" fill="none" stroke="#0b0f1a" stroke-width="1.6"/>', c: '#22d3ee' },
      { name: 'Drift',    icon: '<path d="M3 16c4 0 4-8 9-8s5 8 9 8" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>', c: '#c084fc' }
    ];
    
    function logoHTML(b) {
      return '<a href="#" class="lm-logo" aria-label="' + b.name + '">' +
        '<svg viewBox="0 0 24 24" fill="' + b.c + '">' + b.icon + '</svg>' +
        '<span class="lm-name">' + b.name + '</span></a>';
    }
    
    // Render the brand set twice so the -50% translate loops seamlessly.
    var track = document.getElementById('lmTrack');
    var set = BRANDS.map(logoHTML).join('');
    track.innerHTML = set + set;
    
    // Match animation duration to the number of logos for a steady pace.
    track.style.animationDuration = (BRANDS.length * 3.4) + 's';
  </script>
</body>
</html>
React
import React, { useEffect } from 'react';

// CSS — optionally move to LogoMarquee.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0b0f1a;color:#e2e8f0;display:flex;align-items:center;min-height:100vh}

.lm-section{width:100%;padding:40px 0}
.lm-eyebrow{text-align:center;font-size:12px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:#64748b;margin-bottom:26px}

.lm-marquee{overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 10%,#000 90%,transparent);mask-image:linear-gradient(90deg,transparent,#000 10%,#000 90%,transparent)}
.lm-track{display:flex;align-items:center;gap:56px;width:max-content;animation:lmScroll 26s linear infinite}
.lm-marquee:hover .lm-track{animation-play-state:paused}
@keyframes lmScroll{to{transform:translateX(-50%)}}

.lm-logo{display:flex;align-items:center;gap:10px;color:#94a3b8;flex-shrink:0;transition:color .25s,filter .25s;filter:grayscale(1);opacity:.85}
.lm-logo:hover{color:#fff;filter:grayscale(0);opacity:1}
.lm-logo svg{width:26px;height:26px}
.lm-name{font-size:19px;font-weight:800;letter-spacing:-.02em}
`;

export default function LogoMarquee() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var BRANDS = [
      { name: 'Nimbus',   icon: '<circle cx="12" cy="12" r="9"/><path d="M7 13a5 5 0 0 1 10 0" fill="none" stroke="currentColor" stroke-width="2"/>', c: '#60a5fa' },
      { name: 'Vertex',   icon: '<path d="M12 3 21 19H3z"/>', c: '#a78bfa' },
      { name: 'Pulse',    icon: '<path d="M2 12h5l2-6 4 12 2-6h7" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round"/>', c: '#f472b6' },
      { name: 'Quanta',   icon: '<rect x="4" y="4" width="16" height="16" rx="4"/><circle cx="12" cy="12" r="3" fill="#0b0f1a"/>', c: '#34d399' },
      { name: 'Lumen',    icon: '<path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2"/><circle cx="12" cy="12" r="4"/>', c: '#fbbf24' },
      { name: 'Forge',    icon: '<path d="M4 14h10l4-8 2 4-3 6H4z"/>', c: '#fb7185' },
      { name: 'Atlas',    icon: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18" fill="none" stroke="#0b0f1a" stroke-width="1.6"/>', c: '#22d3ee' },
      { name: 'Drift',    icon: '<path d="M3 16c4 0 4-8 9-8s5 8 9 8" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>', c: '#c084fc' }
    ];
    
    function logoHTML(b) {
      return '<a href="#" class="lm-logo" aria-label="' + b.name + '">' +
        '<svg viewBox="0 0 24 24" fill="' + b.c + '">' + b.icon + '</svg>' +
        '<span class="lm-name">' + b.name + '</span></a>';
    }
    
    // Render the brand set twice so the -50% translate loops seamlessly.
    var track = document.getElementById('lmTrack');
    var set = BRANDS.map(logoHTML).join('');
    track.innerHTML = set + set;
    
    // Match animation duration to the number of logos for a steady pace.
    track.style.animationDuration = (BRANDS.length * 3.4) + 's';
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{css}</style>
      <section className="lm-section">
        <p className="lm-eyebrow">Trusted by teams at</p>
        <div className="lm-marquee" id="lmMarquee">
          <div className="lm-track" id="lmTrack"></div>
        </div>
      </section>
    </>
  );
}
React + Tailwind
import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+

export default function LogoMarquee() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var BRANDS = [
      { name: 'Nimbus',   icon: '<circle cx="12" cy="12" r="9"/><path d="M7 13a5 5 0 0 1 10 0" fill="none" stroke="currentColor" stroke-width="2"/>', c: '#60a5fa' },
      { name: 'Vertex',   icon: '<path d="M12 3 21 19H3z"/>', c: '#a78bfa' },
      { name: 'Pulse',    icon: '<path d="M2 12h5l2-6 4 12 2-6h7" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round"/>', c: '#f472b6' },
      { name: 'Quanta',   icon: '<rect x="4" y="4" width="16" height="16" rx="4"/><circle cx="12" cy="12" r="3" fill="#0b0f1a"/>', c: '#34d399' },
      { name: 'Lumen',    icon: '<path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2"/><circle cx="12" cy="12" r="4"/>', c: '#fbbf24' },
      { name: 'Forge',    icon: '<path d="M4 14h10l4-8 2 4-3 6H4z"/>', c: '#fb7185' },
      { name: 'Atlas',    icon: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18" fill="none" stroke="#0b0f1a" stroke-width="1.6"/>', c: '#22d3ee' },
      { name: 'Drift',    icon: '<path d="M3 16c4 0 4-8 9-8s5 8 9 8" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>', c: '#c084fc' }
    ];
    
    function logoHTML(b) {
      return '<a href="#" class="lm-logo" aria-label="' + b.name + '">' +
        '<svg viewBox="0 0 24 24" fill="' + b.c + '">' + b.icon + '</svg>' +
        '<span class="lm-name">' + b.name + '</span></a>';
    }
    
    // Render the brand set twice so the -50% translate loops seamlessly.
    var track = document.getElementById('lmTrack');
    var set = BRANDS.map(logoHTML).join('');
    track.innerHTML = set + set;
    
    // Match animation duration to the number of logos for a steady pace.
    track.style.animationDuration = (BRANDS.length * 3.4) + 's';
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{`
@keyframes lmScroll{to{transform:translateX(-50%)}}

* {
  box-sizing:border-box;margin:0;padding:0
}

body {
  font-family:system-ui,-apple-system,sans-serif;background:#0b0f1a;color:#e2e8f0;display:flex;align-items:center;min-height:100vh
}

.lm-marquee:hover .lm-track {
  animation-play-state:paused
}

.lm-logo {
  display:flex;align-items:center;gap:10px;color:#94a3b8;flex-shrink:0;transition:color .25s,filter .25s;filter:grayscale(1);opacity:.85
}

.lm-logo:hover {
  color:#fff;filter:grayscale(0);opacity:1
}

.lm-logo svg {
  width:26px;height:26px
}

.lm-name {
  font-size:19px;font-weight:800;letter-spacing:-.02em
}
      `}</style>
      <section className="w-full py-10 px-0">
        <p className="text-center text-xs font-semibold tracking-[.14em] uppercase text-[#64748b] mb-[26px]">Trusted by teams at</p>
        <div className="lm-marquee overflow-hidden [-webkit-mask-image:linear-gradient(90deg,transparent,#000_10%,#000_90%,transparent)] [mask-image:linear-gradient(90deg,transparent,#000_10%,#000_90%,transparent)]" id="lmMarquee">
          <div className="lm-track flex items-center gap-14 w-max [animation:lmScroll_26s_linear_infinite]" id="lmTrack"></div>
        </div>
      </section>
    </>
  );
}
Vue
<template>
  <section class="lm-section">
    <p class="lm-eyebrow">Trusted by teams at</p>
    <div class="lm-marquee" id="lmMarquee">
      <div class="lm-track" id="lmTrack"></div>
    </div>
  </section>
</template>

<script setup>
import { onMounted } from 'vue';

var BRANDS = [
  { name: 'Nimbus',   icon: '<circle cx="12" cy="12" r="9"/><path d="M7 13a5 5 0 0 1 10 0" fill="none" stroke="currentColor" stroke-width="2"/>', c: '#60a5fa' },
  { name: 'Vertex',   icon: '<path d="M12 3 21 19H3z"/>', c: '#a78bfa' },
  { name: 'Pulse',    icon: '<path d="M2 12h5l2-6 4 12 2-6h7" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round"/>', c: '#f472b6' },
  { name: 'Quanta',   icon: '<rect x="4" y="4" width="16" height="16" rx="4"/><circle cx="12" cy="12" r="3" fill="#0b0f1a"/>', c: '#34d399' },
  { name: 'Lumen',    icon: '<path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2"/><circle cx="12" cy="12" r="4"/>', c: '#fbbf24' },
  { name: 'Forge',    icon: '<path d="M4 14h10l4-8 2 4-3 6H4z"/>', c: '#fb7185' },
  { name: 'Atlas',    icon: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18" fill="none" stroke="#0b0f1a" stroke-width="1.6"/>', c: '#22d3ee' },
  { name: 'Drift',    icon: '<path d="M3 16c4 0 4-8 9-8s5 8 9 8" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>', c: '#c084fc' }
];
function logoHTML(b) {
  return '<a href="#" class="lm-logo" aria-label="' + b.name + '">' +
    '<svg viewBox="0 0 24 24" fill="' + b.c + '">' + b.icon + '</svg>' +
    '<span class="lm-name">' + b.name + '</span></a>';
}
let track;
let set;

onMounted(() => {
  track = document.getElementById('lmTrack');
  set = BRANDS.map(logoHTML).join('');
  track.innerHTML = set + set;
  // Match animation duration to the number of logos for a steady pace.
  track.style.animationDuration = (BRANDS.length * 3.4) + 's';
});
</script>

<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0b0f1a;color:#e2e8f0;display:flex;align-items:center;min-height:100vh}

.lm-section{width:100%;padding:40px 0}
.lm-eyebrow{text-align:center;font-size:12px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:#64748b;margin-bottom:26px}

.lm-marquee{overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 10%,#000 90%,transparent);mask-image:linear-gradient(90deg,transparent,#000 10%,#000 90%,transparent)}
.lm-track{display:flex;align-items:center;gap:56px;width:max-content;animation:lmScroll 26s linear infinite}
.lm-marquee:hover .lm-track{animation-play-state:paused}
@keyframes lmScroll{to{transform:translateX(-50%)}}

.lm-logo{display:flex;align-items:center;gap:10px;color:#94a3b8;flex-shrink:0;transition:color .25s,filter .25s;filter:grayscale(1);opacity:.85}
.lm-logo:hover{color:#fff;filter:grayscale(0);opacity:1}
.lm-logo svg{width:26px;height:26px}
.lm-name{font-size:19px;font-weight:800;letter-spacing:-.02em}
</style>
Angular
// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-logo-marquee',
  standalone: true,
  imports: [CommonModule],
  encapsulation: ViewEncapsulation.None,
  template: `
    <section class="lm-section">
      <p class="lm-eyebrow">Trusted by teams at</p>
      <div class="lm-marquee" id="lmMarquee">
        <div class="lm-track" id="lmTrack"></div>
      </div>
    </section>
  `,
  styles: [`
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#0b0f1a;color:#e2e8f0;display:flex;align-items:center;min-height:100vh}
    
    .lm-section{width:100%;padding:40px 0}
    .lm-eyebrow{text-align:center;font-size:12px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:#64748b;margin-bottom:26px}
    
    .lm-marquee{overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 10%,#000 90%,transparent);mask-image:linear-gradient(90deg,transparent,#000 10%,#000 90%,transparent)}
    .lm-track{display:flex;align-items:center;gap:56px;width:max-content;animation:lmScroll 26s linear infinite}
    .lm-marquee:hover .lm-track{animation-play-state:paused}
    @keyframes lmScroll{to{transform:translateX(-50%)}}
    
    .lm-logo{display:flex;align-items:center;gap:10px;color:#94a3b8;flex-shrink:0;transition:color .25s,filter .25s;filter:grayscale(1);opacity:.85}
    .lm-logo:hover{color:#fff;filter:grayscale(0);opacity:1}
    .lm-logo svg{width:26px;height:26px}
    .lm-name{font-size:19px;font-weight:800;letter-spacing:-.02em}
  `]
})
export class LogoMarqueeComponent implements AfterViewInit {
  ngAfterViewInit(): void {
    var BRANDS = [
      { name: 'Nimbus',   icon: '<circle cx="12" cy="12" r="9"/><path d="M7 13a5 5 0 0 1 10 0" fill="none" stroke="currentColor" stroke-width="2"/>', c: '#60a5fa' },
      { name: 'Vertex',   icon: '<path d="M12 3 21 19H3z"/>', c: '#a78bfa' },
      { name: 'Pulse',    icon: '<path d="M2 12h5l2-6 4 12 2-6h7" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round"/>', c: '#f472b6' },
      { name: 'Quanta',   icon: '<rect x="4" y="4" width="16" height="16" rx="4"/><circle cx="12" cy="12" r="3" fill="#0b0f1a"/>', c: '#34d399' },
      { name: 'Lumen',    icon: '<path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2"/><circle cx="12" cy="12" r="4"/>', c: '#fbbf24' },
      { name: 'Forge',    icon: '<path d="M4 14h10l4-8 2 4-3 6H4z"/>', c: '#fb7185' },
      { name: 'Atlas',    icon: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18" fill="none" stroke="#0b0f1a" stroke-width="1.6"/>', c: '#22d3ee' },
      { name: 'Drift',    icon: '<path d="M3 16c4 0 4-8 9-8s5 8 9 8" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>', c: '#c084fc' }
    ];
    
    function logoHTML(b) {
      return '<a href="#" class="lm-logo" aria-label="' + b.name + '">' +
        '<svg viewBox="0 0 24 24" fill="' + b.c + '">' + b.icon + '</svg>' +
        '<span class="lm-name">' + b.name + '</span></a>';
    }
    
    // Render the brand set twice so the -50% translate loops seamlessly.
    var track = document.getElementById('lmTrack');
    var set = BRANDS.map(logoHTML).join('');
    track.innerHTML = set + set;
    
    // Match animation duration to the number of logos for a steady pace.
    track.style.animationDuration = (BRANDS.length * 3.4) + 's';

    // Bind inline-handler functions to the component so the template can call them
    Object.assign(this, { logoHTML });
  }
}