As seen in the field
Astro UI
Logo Marquee.
Logos drifting in a slow loop with faded edges. Pauses on hover, static under reduced motion.
ProofSectionNo dependencies Original, MIT
Style
Using Claude Code? Give it the agent prompt and skip these steps.
-
Import the foundation once
foundation.css holds the tokens every component reads.
--- // src/layouts/Layout.astro import "../styles/foundation.css"; --- -
Use it
--- import LogoMarquee from "../components/viewership/logo-marquee/LogoMarquee.astro"; --- <LogoMarquee />
Props 3
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | · | Short lead-in line. |
items | { name: string; src?: string }[] | · | Logos. Six or more keep the loop seamless. |
duration | number | 40 | Seconds per loop. |
Source 1 file
---
/**
* LogoMarquee: logos drifting in a slow continuous loop, with faded edges.
* Pauses on hover; under reduced motion it becomes a static wrapping row.
*
* @example
* <LogoMarquee title="As seen in the field" items={[{ name: "Kestrel" }]} />
*/
interface Item {
name: string;
src?: string;
}
interface Props {
title?: string;
items: Item[];
/** Seconds for one full loop. */
duration?: number;
class?: string;
}
const { title, items, duration = 40, class: className } = Astro.props;
---
<section class:list={["vs-logo-marquee", className]} aria-label={title ?? "Customers"} style={`--vs-marquee-duration: ${duration}s`}>
<div class="vs-logo-marquee__inner">
{title && <p class="vs-logo-marquee__title">{title}</p>}
<div class="vs-logo-marquee__viewport">
<div class="vs-logo-marquee__track">
{
[0, 1].map((copy) => (
<ul class="vs-logo-marquee__list" aria-hidden={copy === 1 ? "true" : undefined}>
{items.map((it) => (
<li>{it.src ? <img src={it.src} alt={copy === 1 ? "" : it.name} height="28" loading="lazy" /> : <span class="vs-display">{it.name}</span>}</li>
))}
</ul>
))
}
</div>
</div>
</div>
</section>
<style>
.vs-logo-marquee {
container-type: inline-size;
background: var(--vs-page);
color: var(--vs-ink);
font-family: var(--vs-font-sans);
font-stretch: var(--vs-font-stretch);
}
.vs-logo-marquee__inner {
max-width: var(--vs-container);
margin-inline: auto;
padding: clamp(40px, 6cqi, 72px) var(--vs-gutter);
}
.vs-logo-marquee__title {
margin: 0 0 28px;
text-align: center;
font-size: 0.875rem;
color: var(--vs-ink-3);
}
.vs-logo-marquee__viewport {
overflow: hidden;
mask-image: linear-gradient(to right, transparent, #000 12%, #000 88%, transparent);
-webkit-mask-image: linear-gradient(to right, transparent, #000 12%, #000 88%, transparent);
}
.vs-logo-marquee__track {
display: flex;
width: max-content;
animation: vs-logo-marquee-scroll var(--vs-marquee-duration) linear infinite;
}
.vs-logo-marquee__track:hover {
animation-play-state: paused;
}
.vs-logo-marquee__list {
display: flex;
flex: none;
align-items: center;
gap: clamp(40px, 6cqi, 80px);
margin: 0;
padding: 0 clamp(40px, 6cqi, 80px) 0 0;
list-style: none;
color: var(--vs-ink-2);
}
.vs-logo-marquee__list span {
font-size: 1.5rem;
white-space: nowrap;
}
.vs-logo-marquee__list img {
display: block;
width: auto;
height: 28px;
filter: grayscale(1);
opacity: 0.75;
}
@keyframes vs-logo-marquee-scroll {
to {
transform: translateX(-50%);
}
}
@media (prefers-reduced-motion: reduce) {
.vs-logo-marquee__track {
width: auto;
justify-content: center;
animation: none;
}
.vs-logo-marquee__list[aria-hidden="true"] {
display: none;
}
.vs-logo-marquee__list {
flex-wrap: wrap;
justify-content: center;
row-gap: 20px;
padding-right: 0;
}
.vs-logo-marquee__viewport {
mask-image: none;
-webkit-mask-image: none;
}
}
</style> Original, MIT.