Skip to content

Marquee

Marquee creates an infinite horizontal loop for logos, partners or other repeated items. It is CSS-only and positions each child on the same motion path using sibling count and sibling index variables.

View Source 

Installation

Follow instructions for individual framework usage below.

  • winduum 
  • winduum-vue 
  • winduum-react 

Browser support

Item positioning uses sibling-index() — in browsers without support, set the --x-marquee-sibling-index prop on each item as a fallback (see Props).

Styles

Variants

  • default 

Props

  • --x-marquee-sibling-count - total number of marquee items
  • --x-marquee-visible-count - number of visible items used for spacing along the path
  • --x-marquee-inline-size - inline size of each marquee item
  • --x-marquee-gap - gap between marquee items
  • --x-marquee-animation-duration - animation duration, defaults to 30s
  • --x-marquee-animation-timing-function - animation timing function, defaults to linear
  • --x-marquee-sibling-index - item index, required as a fallback for browsers without sibling-index()

Examples

Default

html
<div class="x-marquee flex gap-(--x-marquee-gap) overflow-clip group *:group-has-focus-within:[animation-play-state:paused]" style="--x-marquee-sibling-count: 10; --x-marquee-visible-count: 9; --x-marquee-inline-size: calc(var(--spacing) * 50); --x-marquee-gap: calc(var(--spacing) * 6)">
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" style="--x-marquee-sibling-index: 1">
        <img class="size-full object-contain" src="https://placehold.co/200x56/777777/webp" alt="Partner 1" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" style="--x-marquee-sibling-index: 2">
        <img class="size-full object-contain" src="https://placehold.co/200x56/777777/webp" alt="Partner 2" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" style="--x-marquee-sibling-index: 3">
        <img class="size-full object-contain" src="https://placehold.co/200x56/888888/webp" alt="Partner 3" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" style="--x-marquee-sibling-index: 4">
        <img class="size-full object-contain" src="https://placehold.co/200x56/333333/webp" alt="Partner 4" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" aria-hidden="true" tabindex="-1" style="--x-marquee-sibling-index: 5">
        <img class="size-full object-contain" src="https://placehold.co/200x56/333333/webp" alt="Partner 5" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" aria-hidden="true" tabindex="-1" style="--x-marquee-sibling-index: 6">
        <img class="size-full object-contain" src="https://placehold.co/200x56/111111/webp" alt="Partner 6" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" aria-hidden="true" tabindex="-1" style="--x-marquee-sibling-index: 7">
        <img class="size-full object-contain" src="https://placehold.co/200x56/444444/webp" alt="Partner 7" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" aria-hidden="true" tabindex="-1" style="--x-marquee-sibling-index: 8">
        <img class="size-full object-contain" src="https://placehold.co/200x56/444444/webp" alt="Partner 8" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" aria-hidden="true" tabindex="-1" style="--x-marquee-sibling-index: 9">
        <img class="size-full object-contain" src="https://placehold.co/200x56/888888/webp" alt="Partner 9" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" aria-hidden="true" tabindex="-1" style="--x-marquee-sibling-index: 10">
        <img class="size-full object-contain" src="https://placehold.co/200x56/222222/webp" alt="Partner 10" width="200" height="56" loading="lazy">
    </a>
</div>
vue
<script setup lang="ts">
    import { Marquee } from '@/components/marquee'

    const partners = [
        '777777',
        '777777',
        '888888',
        '333333',
        '333333',
        '111111',
        '444444',
        '444444',
        '888888',
        '222222'
    ]
</script>

<template>
    <Marquee
        class="flex gap-(--x-marquee-gap) overflow-clip group *:group-has-focus-within:[animation-play-state:paused]"
        :style="{
            '--x-marquee-sibling-count': partners.length,
            '--x-marquee-visible-count': 9,
            '--x-marquee-inline-size': 'calc(var(--spacing) * 50)',
            '--x-marquee-gap': 'calc(var(--spacing) * 6)'
        }"
    >
        <a
            v-for="(color, index) in partners"
            href="#"
            class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent"
            :aria-hidden="index > 3 ? 'true' : undefined"
            :tabindex="index > 3 ? -1 : undefined"
            :style="{ '--x-marquee-sibling-index': index + 1 }"
        >
            <img class="size-full object-contain" :src="`https://placehold.co/200x56/${color}/webp`" :alt="`Partner ${index + 1}`" width="200" height="56" loading="lazy">
        </a>
    </Marquee>
</template>
jsx
import type { CSSProperties } from 'react'
import { Marquee } from '@/components/marquee'

const partners = [
    '777777',
    '777777',
    '888888',
    '333333',
    '333333',
    '111111',
    '444444',
    '444444',
    '888888',
    '222222'
]

const marqueeStyle = {
    '--x-marquee-sibling-count': partners.length,
    '--x-marquee-visible-count': 9,
    '--x-marquee-inline-size': 'calc(var(--spacing) * 50)',
    '--x-marquee-gap': 'calc(var(--spacing) * 6)'
} as CSSProperties

export function Example() {
    return (
        <Marquee className="flex gap-(--x-marquee-gap) overflow-clip group *:group-has-focus-within:[animation-play-state:paused]" style={marqueeStyle}>
            {partners.map((color, index) => (
                <a
                    key={index}
                    href="#"
                    className="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent"
                    aria-hidden={index > 3 ? true : undefined}
                    tabIndex={index > 3 ? -1 : undefined}
                    style={{ '--x-marquee-sibling-index': index + 1 } as CSSProperties}
                >
                    <img className="size-full object-contain" src={`https://placehold.co/200x56/${color}/webp`} alt={`Partner ${index + 1}`} width="200" height="56" loading="lazy" />
                </a>
            ))}
        </Marquee>
    )
}

Reverse

Use TailwindCSS arbitrary child variant *:[animation-direction:reverse] to move the loop in the opposite direction.

html
<div class="x-marquee flex gap-(--x-marquee-gap) overflow-clip group *:group-has-focus-within:[animation-play-state:paused] *:[animation-direction:reverse]" style="--x-marquee-sibling-count: 10; --x-marquee-visible-count: 9; --x-marquee-inline-size: calc(var(--spacing) * 50); --x-marquee-gap: calc(var(--spacing) * 6)">
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" style="--x-marquee-sibling-index: 1">
        <img class="size-full object-contain" src="https://placehold.co/200x56/777777/webp" alt="Partner 1" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" style="--x-marquee-sibling-index: 2">
        <img class="size-full object-contain" src="https://placehold.co/200x56/777777/webp" alt="Partner 2" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" style="--x-marquee-sibling-index: 3">
        <img class="size-full object-contain" src="https://placehold.co/200x56/888888/webp" alt="Partner 3" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" style="--x-marquee-sibling-index: 4">
        <img class="size-full object-contain" src="https://placehold.co/200x56/333333/webp" alt="Partner 4" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" aria-hidden="true" tabindex="-1" style="--x-marquee-sibling-index: 5">
        <img class="size-full object-contain" src="https://placehold.co/200x56/333333/webp" alt="Partner 5" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" aria-hidden="true" tabindex="-1" style="--x-marquee-sibling-index: 6">
        <img class="size-full object-contain" src="https://placehold.co/200x56/111111/webp" alt="Partner 6" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" aria-hidden="true" tabindex="-1" style="--x-marquee-sibling-index: 7">
        <img class="size-full object-contain" src="https://placehold.co/200x56/444444/webp" alt="Partner 7" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" aria-hidden="true" tabindex="-1" style="--x-marquee-sibling-index: 8">
        <img class="size-full object-contain" src="https://placehold.co/200x56/444444/webp" alt="Partner 8" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" aria-hidden="true" tabindex="-1" style="--x-marquee-sibling-index: 9">
        <img class="size-full object-contain" src="https://placehold.co/200x56/888888/webp" alt="Partner 9" width="200" height="56" loading="lazy">
    </a>
    <a href="#" class="w-(--x-marquee-inline-size) h-14 shrink-0 outline-2 outline-transparent outline-dashed rounded-lg focus-visible:outline-accent" aria-hidden="true" tabindex="-1" style="--x-marquee-sibling-index: 10">
        <img class="size-full object-contain" src="https://placehold.co/200x56/222222/webp" alt="Partner 10" width="200" height="56" loading="lazy">
    </a>
</div>

Accessibility

Keep only the real set of links focusable. If extra duplicated items are present only to make the loop feel continuous, add aria-hidden="true" and tabindex="-1" to those duplicates.

Pause the animation when focus moves inside the marquee so keyboard users can interact with links without the focused item moving away. The examples use *:group-has-focus-within:[animation-play-state:paused] on the marquee wrapper.

Released under the MIT License.