Skip to content
Velocity X Docs
← All components

Logo Ticker

Free · MIT

CSS-only infinite marquee of client/partner logos that loops seamlessly in either direction and pauses on hover or focus.

Category
Carousels & Tickers
Framework
astro
Version
1.0.0
Peer deps
none

Install

bash
velocityx add logo-ticker

Free and MIT licensed — no account needed. The file lands under src/components/velocity/logo-ticker/ by default (override with --dest), and it's yours to edit from the moment it's written.

Demo

View the live demo on the showcase site ↗

Source

Pulled live from the registry — this is the exact file velocityx add logo-ticker writes into your project right now.

components/velocity/logo-ticker/LogoTicker.astro

astro
---
//    VELOCITY COMPONENT · theming
// 
//    Colours here are Tailwind `primary-*` / `accent-*`, which resolve through
//    the Velocity preset. Out of the box they are a neutral indigo — NOT the
//    Aidxn brand — so this drops into your project without dragging our look in.
// 
//    ⚡ NOT USING VELOCITY? Run this once, in your project root:
// 
//          velocityx theme
// 
//    It writes velocity-preset.cjs + velocity-theme.css. Add the preset to your
//    tailwind.config, import the css, then set your whole brand in ONE place:
// 
//          :root {
//            --vx-brand: custom;
//            --vx-primary-600: 220 38 38;   <- your action colour
//            --vx-primary-700: 185 28 28;
//          }
// 
//    ⚠️  Raw RGB channels, space separated — NOT hex. `#DC2626` here silently
//        breaks every /opacity variant in the library (bg-primary-600/20 etc).
// 
//    Prefer not to install anything? Find-and-replace `primary-` with your own
//    scale name. Nothing else in this file depends on Velocity.
/**
 * LogoTicker — infinite CSS-only marquee for client/partner logos.
 *
 * Zero JS. Duplicates the logo set once so the track can loop seamlessly
 * (translate -50%), direction is a CSS custom property so left/right share
 * one keyframe. Pauses on hover/focus and stops completely under
 * prefers-reduced-motion (shows a static wrapped row instead of motion).
 *
 * Usage:
 *   <LogoTicker
 *     logos={[{ src: "/logos/a.svg", alt: "Acme" }, ...]}
 *     direction="left"
 *     speed={30}
 *   />
 */
export interface LogoItem {
  src: string;
  alt: string;
  href?: string;
}

export interface Props {
  logos: LogoItem[];
  direction?: 'left' | 'right';
  speed?: number; // seconds for one full loop
  grayscale?: boolean;
}

const { logos, direction = 'left', speed = 28, grayscale = true } = Astro.props;
const uid = `lt-${Math.random().toString(36).slice(2, 9)}`;
---

<div
  class="relative w-full overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_8%,black_92%,transparent)]"
  role="region"
  aria-label="Logo ticker"
>
  <div
    class={`${uid} flex w-max items-center gap-10 sm:gap-14 py-4 motion-reduce:animate-none`}
    style={`--lt-duration:${speed}s; --lt-direction:${direction === 'left' ? 'normal' : 'reverse'};`}
  >
    {[0, 1].map(() => (
      <div class="flex items-center gap-10 sm:gap-14 shrink-0" aria-hidden={logos.length > 0}>
        {logos.map((logo) => (
          logo.href ? (
            <a
              href={logo.href}
              class="shrink-0 opacity-70 transition-opacity hover:opacity-100 focus-visible:opacity-100 outline-none focus-visible:ring-2 focus-visible:ring-primary-500 rounded-md"
              tabindex="-1"
            >
              <img
                src={logo.src}
                alt={logo.alt}
                class:list={['h-7 sm:h-8 w-auto select-none', grayscale && 'grayscale hover:grayscale-0 focus-visible:grayscale-0 transition-[filter]']}
                loading="lazy"
                draggable="false"
              />
            </a>
          ) : (
            <img
              src={logo.src}
              alt={logo.alt}
              class:list={['h-7 sm:h-8 w-auto select-none shrink-0 opacity-70', grayscale && 'grayscale']}
              loading="lazy"
              draggable="false"
            />
          )
        ))}
      </div>
    ))}
  </div>
</div>

<style is:global>
  @keyframes v-ticker-scroll {
    from { transform: translateX(0); }
    to { transform: translateX(-50%); }
  }
</style>

<style set:html={`
  .${uid} {
    animation: v-ticker-scroll var(--lt-duration) linear infinite;
    animation-direction: var(--lt-direction);
  }
  .${uid}:hover, .${uid}:focus-within {
    animation-play-state: paused;
  }
  @media (prefers-reduced-motion: reduce) {
    .${uid} {
      animation: none !important;
      flex-wrap: wrap;
      width: 100%;
    }
    .${uid} > div:nth-child(2) {
      display: none;
    }
  }
`}></style>

Theming

Uses the shared primary-* / accent-* scale — see Theming if colours look unstyled after install.