Skip to content
A Astro Rocket
astro-rocket animation css components ux performance

The Stack Marquee — A Pure-CSS Infinite Tech Strip

Astro Rocket's homepage has a new Stack Marquee: two rows of tech-stack cards scrolling opposite ways with zero JavaScript. Here's how the loop works and why it's hidden on phones.

H

Hans Martens

3 min read

Astro Rocket’s homepage has a new section, tucked in just above the call to action: the Stack Marquee. It’s two rows of tech-stack cards that scroll in opposite directions — an endless, soft-edged strip of the tools the theme is built on. It runs on pure CSS, recolours itself with your active theme, and is driven entirely by the src/content/stack collection. It’s also deliberately absent on phones. Here’s how all of that works.

A seamless loop, with zero JavaScript

The trick behind every infinite marquee is duplication. Each row holds its list of cards twice over, and a single CSS keyframe slides the whole track left by exactly half its width. Because the second copy is identical to the first, the instant the animation loops back to the start there’s nothing to give it away — the join is invisible.

@keyframes stack-marquee-scroll {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(calc(-50% - (var(--marquee-gap) / 2)), 0, 0); }
}

There’s no JavaScript, no requestAnimationFrame, no scroll listener — just a GPU-friendly transform animation that the browser can run on its own thread.

The two rows don’t move as one. The first row scrolls one way; the second reverses the list, flips its direction, and runs a touch faster, so the lanes never march in lockstep. The edges are masked with a CSS gradient so cards fade in and out instead of popping at the boundary, and hovering a row pauses it so you can read — or click — a card.

Filling the loop on wide screens

The -50% trick only hides the join when one copy of the row is at least as wide as the container. The theme ships with four tools, and on a wide desktop four cards aren’t quite wide enough to span the screen — so the loop would flash a small gap at the seam (tablets and phones, being narrower, were fine).

The fix is to repeat the list up to a minimum card count before it gets duplicated, so a single copy always overflows even the widest container. The maths above is untouched, because the track is still exactly two identical copies:

const MIN_CARDS = 8;
const fill = (list) => {
  if (!list.length) return list;
  const out = [...list];
  while (out.length < MIN_CARDS) out.push(...list);
  return out;
};

Driven by your content

Every card comes from src/content/stack — one MDX file per tool, each with a name, an icon, and a link. Add or remove a tool by editing that folder and both the marquee and the static tech grid on the About page update on their own; there’s nothing to wire up in the component.

Each card is the same Card and Icon you’ll find everywhere else in the component library, so it inherits the theme’s spacing, borders, and — through a single brand token — its colour. Switch themes and the entire strip recolours instantly.

Why it sits out on phones

On a phone, the marquee is hidden entirely. A horizontally-scrolling, auto-animating strip wants room: on a narrow screen only a card or two fit at a time, the effect is lost, and a constantly-moving block competes for attention while someone is trying to read. Rather than ship a cramped version, the homepage simply leaves it out below tablet size — the page reads cleaner and renders a little less.

Catching every phone takes two rules. Portrait phones are the easy half — they’re narrow, so a width query handles them:

@media (max-width: 767px) {
  .stack-marquee-section { display: none; }
}

Landscape phones are the tricky half. They’re wider than the tablet breakpoint, so a width query alone would let them slip through and show the marquee. What gives them away is that they’re short and touch-driven, so a second rule catches them by their height and pointer type — without ever touching a real tablet or a laptop:

@media (orientation: landscape) and (max-height: 500px) and (pointer: coarse) {
  .stack-marquee-section { display: none; }
}

Keeping the zebra intact

The homepage alternates two background tones section by section — a zebra stripe running down the page. Dropping a section in or out could easily break that rhythm, so the marquee is careful about it.

When it’s hidden, the whole <section> is removed from the flow with display: none (not just visually hidden), so the Blog section and the CTA become direct neighbours and keep alternating. When it’s shown, it slots in as a background stripe, and the CTA and footer below it swap tones so the run stays perfectly two-coloured from top to bottom — no third colour required. If you want the tokens behind those two tones, the colour token system post covers them.

It respects reduced motion

Like every piece of motion in the theme, the marquee defers to prefers-reduced-motion. If a visitor has asked their system to go easy on animation, the scroll is switched off and each row becomes a normal, manually-scrollable strip — all the content is still there, it just stops moving on its own:

@media (prefers-reduced-motion: reduce) {
  .stack-marquee-track { animation: none; transform: none; }
  .stack-marquee-row { overflow-x: auto; }
}

For more on how the theme handles motion responsibly, see Animations in Astro Rocket.

Using it

The component lives at src/components/landing/StackMarquee.astro. Drop it into any section and it pulls from your stack collection automatically:

<StackMarquee />

It accepts props for the number of rows, scroll speed, card width, edge fade, and whether the cards link out — but the defaults are tuned for the homepage, where it sits in its own band directly above the CTA.

Share:

Related Posts

Animations in Astro Rocket — How the Premium Motion Design Works

A deep dive into every animation layer in Astro Rocket: the spring-curve hero entrance, staggered cascades, scroll-reveal, and how it all stays smooth without a JS animation library.

H Hans Martens
2 min read
astro-rocket animation css ux performance

Hero Scroll Indicator — Desktop-Only, Hides on Scroll

Astro Rocket's hero has an animated scroll indicator: two bouncing chevrons that fade in after the hero animation and disappear the moment you start scrolling. Here's how every part of it works.

H Hans Martens
2 min read
astro-rocket features ux animation

Scroll Progress Ring — A Circular Indicator on the Back-to-Top Button

Astro Rocket's back-to-top button now has a circular SVG progress ring that fills as you scroll. It's brand-coloured, theme-aware, and runs entirely in CSS and a small inline script.

H Hans Martens
2 min read
astro-rocket features ux animation

Follow along

Stay in the loop — new articles, thoughts, and updates.