Vue componentFree

Logo Cloud

"Trusted by" logo cloud for Vue 3 with a bordered grid or an endless marquee, using text wordmarks and SVG marks. Tailwind CSS, dark mode, reduced motion.

by Vueplayv1.0.0Updated 3 hours ago

Install
Full screen ↗

About this component

A social-proof strip for landing pages: a short line like "Trusted by 2,000+ teams" followed by the brands that use your product, either as a tidy bordered grid or as a slowly scrolling marquee.

The placeholder brands are fictional wordmarks built from text and simple SVG shapes, each with its own type treatment, so the section looks finished before you have real logos. Swap in your customers by passing logos, or replace the markup with your own SVG files. Add an href to link a logo to its case study.

The marquee is animated with the Web Animations API, so there are no global keyframes to add. It pauses on hover and focus, stops entirely when the visitor prefers reduced motion, and hides the duplicated loop from screen readers.

  • Grid and marquee variants
  • Fictional text + SVG wordmarks
  • Pauses on hover, respects reduced motion
  • Optional links per logo
  • Light and dark mode

Code

11 files in the repo
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'

/** Simple geometric marks drawn next to each wordmark. */
export type LogoMark = 'circle' | 'triangle' | 'square' | 'hexagon' | 'bolt' | 'wave' | 'ring' | 'none'

export interface Logo {
  name: string
  mark?: LogoMark
  /** Optional link, e.g. to a case study. */
  href?: string
}

const props = withDefaults(defineProps<{
  heading?: string
  logos?: Logo[]
  /** 'grid' shows a static bordered grid; 'marquee' scrolls the logos in an endless loop. */
  variant?: 'grid' | 'marquee'
  /** Seconds for one full marquee loop. */
  speed?: number
  /** Pause the marquee while hovered or focused. */
  pauseOnHover?: boolean
}>(), {
  heading: 'Trusted by 2,000+ product teams, from two-person startups to public companies',
  variant: 'grid',
  speed: 40,
  pauseOnHover: true,
  logos: () => [
    { name: 'Northwind', mark: 'triangle' },
    { name: 'Halcyon', mark: 'circle' },
    { name: 'Copperleaf', mark: 'hexagon' },
    { name: 'Voltic', mark: 'bolt' },
    { name: 'Tidewater', mark: 'wave' },
    { name: 'Orbitly', mark: 'ring' },
    { name: 'Blockhaus', mark: 'square' },
    { name: 'Mistral Bay', mark: 'none' },
  ],
})

// Each wordmark gets its own type treatment so the row reads like a set of distinct brands.
const typeStyles = [
  'font-bold tracking-tight',
  'font-serif font-semibold italic',
  'font-extrabold uppercase tracking-wider text-[0.95em]',
  'font-black italic tracking-tighter',
  'font-medium lowercase tracking-tight',
  'font-mono font-bold',
  'font-extrabold uppercase tracking-[0.18em] text-[0.85em]',
  'font-serif font-bold',
]

const marks: Record<Exclude<LogoMark, 'none'>, string> = {
  circle: 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Z',
  triangle: 'M12 3 22 20H2L12 3Z',
  square: 'M4 4h16v16H4z',
  hexagon: 'M12 2.5 20.5 7.25v9.5L12 21.5l-8.5-4.75v-9.5L12 2.5Z',
  bolt: 'M13.5 2 4 14h7l-1.5 8L20 10h-7l.5-8Z',
  wave: 'M2 14c2.5-4 5-4 7.5 0s5 4 7.5 0 3.5-3 5-2.5V20H2v-6Z',
  ring: 'M12 22a10 10 0 1 1 0-20 10 10 0 0 1 0 20Zm0-5a5 5 0 1 0 0-10 5 5 0 0 0 0 10Z',
}

// Marquee: animated with the Web Animations API so the component needs no global CSS keyframes.
const track = ref<HTMLElement>()
let animation: Animation | undefined

function startMarquee() {
  animation?.cancel()
  animation = undefined
  if (props.variant !== 'marquee' || !track.value) return
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
  animation = track.value.animate(
    [{ transform: 'translateX(0)' }, { transform: 'translateX(-50%)' }],
    { duration: props.speed * 1000, iterations: Infinity, easing: 'linear' },
  )
}
function pause() { if (props.pauseOnHover) animation?.pause() }
function resume() { animation?.play() }

onMounted(startMarquee)
watch(() => [props.variant, props.speed, props.logos], () => requestAnimationFrame(startMarquee))
onBeforeUnmount(() => animation?.cancel())
</script>

<template>
  <section class="px-6 py-16 sm:py-20 lg:px-8">
    <h2 v-if="heading" class="mx-auto max-w-2xl text-center text-sm font-semibold text-balance text-gray-600 dark:text-gray-400">{{ heading }}</h2>

    <!-- Grid -->
    <ul
      v-if="variant === 'grid'"
      class="mx-auto mt-10 grid max-w-6xl grid-cols-2 gap-px overflow-hidden rounded-2xl bg-gray-200 ring-1 ring-gray-200 md:grid-cols-4 dark:bg-gray-800 dark:ring-gray-800"
    >
      <li v-for="(logo, i) in logos" :key="logo.name" class="flex bg-white dark:bg-gray-950">
        <component
          :is="logo.href ? 'a' : 'div'"
          :href="logo.href"
          class="group flex h-24 w-full items-center justify-center gap-2 px-4 text-gray-500 transition hover:bg-gray-50 hover:text-gray-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600 sm:h-28 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-white"
        >
          <svg v-if="logo.mark && logo.mark !== 'none'" viewBox="0 0 24 24" aria-hidden="true" class="size-6 shrink-0 fill-current">
            <path :d="marks[logo.mark]" fill-rule="evenodd" />
          </svg>
          <span class="text-lg sm:text-xl" :class="typeStyles[i % typeStyles.length]">{{ logo.name }}</span>
        </component>
      </li>
    </ul>

    <!-- Marquee -->
    <div
      v-else
      class="relative mx-auto mt-10 max-w-6xl overflow-hidden mask-[linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]"
      @mouseenter="pause"
      @mouseleave="resume"
      @focusin="pause"
      @focusout="resume"
    >
      <div ref="track" class="flex w-max">
        <!-- The list is rendered twice so translating by -50% loops seamlessly; the copy is hidden from assistive tech. -->
        <ul v-for="copy in 2" :key="copy" class="flex shrink-0 items-center gap-12 pr-12 sm:gap-16 sm:pr-16" :aria-hidden="copy === 2 ? 'true' : undefined">
          <li v-for="(logo, i) in logos" :key="logo.name">
            <component
              :is="logo.href ? 'a' : 'div'"
              :href="logo.href"
              :tabindex="copy === 2 && logo.href ? -1 : undefined"
              class="flex items-center gap-2 whitespace-nowrap text-gray-500 transition hover:text-gray-900 focus-visible:rounded focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-sky-600 dark:text-gray-400 dark:hover:text-white"
            >
              <svg v-if="logo.mark && logo.mark !== 'none'" viewBox="0 0 24 24" aria-hidden="true" class="size-6 shrink-0 fill-current">
                <path :d="marks[logo.mark]" fill-rule="evenodd" />
              </svg>
              <span class="text-xl" :class="typeStyles[i % typeStyles.length]">{{ logo.name }}</span>
            </component>
          </li>
        </ul>
      </div>
    </div>

    <div v-if="$slots.default" class="mt-10 text-center">
      <slot />
    </div>
  </section>
</template>

Props

PropTypeDefaultDescription
headingstring'Trusted by 2,000+ product teams…'Caption above the logos. Empty string hides it.
logosLogo[]8 fictional brandsLogos with name, optional mark ('circle' | 'triangle' | 'square' | 'hexagon' | 'bolt' | 'wave' | 'ring' | 'none') and optional href.
variant'grid' | 'marquee''grid'Static bordered grid or endlessly scrolling row.
speednumber40Seconds for one full marquee loop. Higher is slower.
pauseOnHoverbooleantruePause the marquee while it is hovered or focused.

Slots

SlotDescription
defaultContent centred under the logos, e.g. a link to customer stories.
All feature sections →
Preview of Feature AlternatingFree

Component · Feature sections

Feature Alternating

Alternating Vue 3 feature rows that zigzag text and CSS-drawn illustrations, with bullets and links per row. Tailwind CSS, dark mode, no images needed.

by Vueplay
Preview of Feature GridFree

Component · Feature sections

Feature Grid

Responsive Vue 3 feature grid with a heading, inline SVG icons and plain or card variants in 2, 3 or 4 columns. Tailwind CSS, dark mode, typed data.

by Vueplay
Preview of Bento GridFree

Component · Feature sections

Bento Grid

Bento-style Vue 3 feature grid with wide and tall tiles and small CSS-drawn visuals like maps, timelines and code. Tailwind CSS, dark mode, typed tiles.

by Vueplay
Preview of Hero CenteredFree

Component · Hero sections

Hero Centered

Centered Vue 3 hero with an announcement pill, gradient headline, grid background, two CTAs and a row of wordmarks. Tailwind CSS with dark mode.

by Vueplay

Pro feature

Upgrade to Pro

Upgrade to unlock more of Vueplay. Cancel any time.

  • Custom domains with HTTPS
  • Private projects
  • No "Made with Vueplay" badge
  • GitHub sync

$120/year

Compare plansBilling settings