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 SourceInstallation
Follow instructions for individual framework usage below.
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
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 to30s--x-marquee-animation-timing-function- animation timing function, defaults tolinear--x-marquee-sibling-index- item index, required as a fallback for browsers withoutsibling-index()
Examples
Default
<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><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>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.
<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.