12 CSS Infinite Marquee Designs10 / 12

Pure CSSMIT licensed

Vertical Infinite Marquee — Agency Services Column

The agency-hero staple: a vertical column of services scrolling upward beside bold static copy, with a top-and-bottom mask fade and one 'live' highlighted row at center height — the same duplicated-group math as horizontal marquees, rotated 90 degrees with flex-direction:column and translateY.

Published

Live Demo
Try it

The code

<section class="mqs-10" aria-label="Vertical marquee demo">
  <div class="mqs-10__inner">
    <div class="mqs-10__copy">
      <p class="mqs-10__eyebrow">Full-service studio</p>
      <h3 class="mqs-10__hero">We do the whole thing.</h3>
      <p class="mqs-10__sub">Strategy through ship. One team, no handoffs.</p>
    </div>
    <div class="mqs-10__col">
      <div class="mqs-10__band" aria-hidden="true"></div>
      <div class="mqs-10__viewport">
        <div class="mqs-10__group">
          <span class="mqs-10__row">Brand identity</span>
          <span class="mqs-10__row">Web design</span>
          <span class="mqs-10__row">Product strategy</span>
          <span class="mqs-10__row">Motion &amp; 3D</span>
          <span class="mqs-10__row">Design systems</span>
          <span class="mqs-10__row">Development</span>
        </div>
        <div class="mqs-10__group" aria-hidden="true">
          <span class="mqs-10__row">Brand identity</span>
          <span class="mqs-10__row">Web design</span>
          <span class="mqs-10__row">Product strategy</span>
          <span class="mqs-10__row">Motion &amp; 3D</span>
          <span class="mqs-10__row">Design systems</span>
          <span class="mqs-10__row">Development</span>
        </div>
      </div>
    </div>
  </div>
</section>
.mqs-10,
.mqs-10 *,
.mqs-10 *::before,
.mqs-10 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.mqs-10 {
  min-height: 100vh;
  width: 100%;
  --gap: 0px;
  --speed: 16s;
  --accent: #4f46e5;
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: #f4f4f8;
  padding: 64px 24px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.mqs-10__inner {
  width: min(880px,100%);
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 40px;
}

.mqs-10__eyebrow {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 12px;
}

.mqs-10__hero {
  font-size: clamp(28px,4.5vw,42px);
  font-weight: 800;
  color: #18181b;
  letter-spacing: -.03em;
  line-height: 1.05;
  text-wrap: balance;
}

.mqs-10__sub {
  font-size: 14.5px;
  color: #71717a;
  margin-top: 12px;
  max-width: 36ch;
  text-wrap: pretty;
}

.mqs-10__col {
  position: relative;
  width: min(300px,42vw);
}

.mqs-10__band {
  position: absolute;
  left: -10px;
  right: -10px;
  top: 50%;
  height: 64px;
  translate: 0 -50%;
  border-block: 2px solid var(--accent);
  background: rgb(79 70 229 / .06);
  border-radius: 4px;
  z-index: 1;
  pointer-events: none;
}

.mqs-10__viewport {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  height: 340px;
  overflow: hidden;
  mask-image: linear-gradient(180deg,transparent,#000 18%,#000 82%,transparent);
  -webkit-mask-image: linear-gradient(180deg,transparent,#000 18%,#000 82%,transparent);
}

.mqs-10__group {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  animation: mqs-10-scroll var(--speed) linear infinite;
  will-change: transform;
}

@keyframes mqs-10-scroll {
  to {
    transform: translateY(calc(-100% - var(--gap)));
  }
}

.mqs-10__col:hover .mqs-10__group,
.mqs-10__col:focus-within .mqs-10__group {
  animation-play-state: paused;
}

.mqs-10__row {
  height: 64px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 19px;
  font-weight: 700;
  color: #3f3f46;
  letter-spacing: -.01em;
}

@media (max-width: 640px) {
  .mqs-10__inner {
    grid-template-columns: 1fr;
    justify-items: center;
    text-align: center;
  }

  .mqs-10__viewport {
    max-height: 45vh;
  }
}

@media (prefers-reduced-motion: reduce) {
  .mqs-10__group {
    animation: none;
  }

  .mqs-10__viewport {
    overflow-y: auto;
    mask-image: none;
    -webkit-mask-image: none;
  }

  .mqs-10__band {
    display: none;
  }
}
/* No JavaScript — the horizontal pattern rotated: flex-direction:column, an explicit viewport height, translateY(calc(-100% - var(--gap))), and a 180deg mask. */
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Infinite Marquee Design from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Vertical Infinite Marquee — Agency Services Column
Source: https://codefronts.com/motion/css-infinite-marquee/vertical-marquee-agency-services/

The agency-hero staple: a vertical column of services scrolling upward beside bold static copy, with a top-and-bottom mask fade and one 'live' highlighted row at center height — the same duplicated-group math as horizontal marquees, rotated 90 degrees with flex-direction:column and translateY.
## HTML
```html
<section class="mqs-10" aria-label="Vertical marquee demo">
  <div class="mqs-10__inner">
    <div class="mqs-10__copy">
      <p class="mqs-10__eyebrow">Full-service studio</p>
      <h3 class="mqs-10__hero">We do the whole thing.</h3>
      <p class="mqs-10__sub">Strategy through ship. One team, no handoffs.</p>
    </div>
    <div class="mqs-10__col">
      <div class="mqs-10__band" aria-hidden="true"></div>
      <div class="mqs-10__viewport">
        <div class="mqs-10__group">
          <span class="mqs-10__row">Brand identity</span>
          <span class="mqs-10__row">Web design</span>
          <span class="mqs-10__row">Product strategy</span>
          <span class="mqs-10__row">Motion &amp; 3D</span>
          <span class="mqs-10__row">Design systems</span>
          <span class="mqs-10__row">Development</span>
        </div>
        <div class="mqs-10__group" aria-hidden="true">
          <span class="mqs-10__row">Brand identity</span>
          <span class="mqs-10__row">Web design</span>
          <span class="mqs-10__row">Product strategy</span>
          <span class="mqs-10__row">Motion &amp; 3D</span>
          <span class="mqs-10__row">Design systems</span>
          <span class="mqs-10__row">Development</span>
        </div>
      </div>
    </div>
  </div>
</section>
```
## CSS
```css
.mqs-10,
.mqs-10 *,
.mqs-10 *::before,
.mqs-10 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.mqs-10 {
  min-height: 100vh;
  width: 100%;
  --gap: 0px;
  --speed: 16s;
  --accent: #4f46e5;
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: #f4f4f8;
  padding: 64px 24px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.mqs-10__inner {
  width: min(880px,100%);
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 40px;
}

.mqs-10__eyebrow {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 12px;
}

.mqs-10__hero {
  font-size: clamp(28px,4.5vw,42px);
  font-weight: 800;
  color: #18181b;
  letter-spacing: -.03em;
  line-height: 1.05;
  text-wrap: balance;
}

.mqs-10__sub {
  font-size: 14.5px;
  color: #71717a;
  margin-top: 12px;
  max-width: 36ch;
  text-wrap: pretty;
}

.mqs-10__col {
  position: relative;
  width: min(300px,42vw);
}

.mqs-10__band {
  position: absolute;
  left: -10px;
  right: -10px;
  top: 50%;
  height: 64px;
  translate: 0 -50%;
  border-block: 2px solid var(--accent);
  background: rgb(79 70 229 / .06);
  border-radius: 4px;
  z-index: 1;
  pointer-events: none;
}

.mqs-10__viewport {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  height: 340px;
  overflow: hidden;
  mask-image: linear-gradient(180deg,transparent,#000 18%,#000 82%,transparent);
  -webkit-mask-image: linear-gradient(180deg,transparent,#000 18%,#000 82%,transparent);
}

.mqs-10__group {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  animation: mqs-10-scroll var(--speed) linear infinite;
  will-change: transform;
}

@keyframes mqs-10-scroll {
  to {
    transform: translateY(calc(-100% - var(--gap)));
  }
}

.mqs-10__col:hover .mqs-10__group,
.mqs-10__col:focus-within .mqs-10__group {
  animation-play-state: paused;
}

.mqs-10__row {
  height: 64px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 19px;
  font-weight: 700;
  color: #3f3f46;
  letter-spacing: -.01em;
}

@media (max-width: 640px) {
  .mqs-10__inner {
    grid-template-columns: 1fr;
    justify-items: center;
    text-align: center;
  }

  .mqs-10__viewport {
    max-height: 45vh;
  }
}

@media (prefers-reduced-motion: reduce) {
  .mqs-10__group {
    animation: none;
  }

  .mqs-10__viewport {
    overflow-y: auto;
    mask-image: none;
    -webkit-mask-image: none;
  }

  .mqs-10__band {
    display: none;
  }
}
```

## JavaScript
```js
/* No JavaScript — the horizontal pattern rotated: flex-direction:column, an explicit viewport height, translateY(calc(-100% - var(--gap))), and a 180deg mask. */
```

How this works

Everything transfers from the horizontal pattern with three substitutions: the viewport gets a fixed height (vertical loops must be clipped by an explicit block size — there is no 'natural' height limit like a page width), groups stack with flex-direction:column, and the keyframe becomes translateY(calc(-100% - var(--gap))). The mask rotates too: linear-gradient(180deg, transparent, #000 18%, #000 82%, transparent).

The center highlight is a positioned overlay — a fixed horizontal band at 50% height with a border and accent tint. Rows scroll beneath it, so whichever service crosses center appears momentarily 'selected'. It's a pure-CSS way to fake the 'slot machine' focus effect that usually needs IntersectionObserver.

Line heights are uniform (height:64px per row) so the rhythm under the highlight band is steady. As always: duplicate group aria-hidden, hover/focus pause, and the reduced-motion fallback turns the column into a normal scrollable list with the mask removed.

Techniques used: vertical duplicated-group loop (flex-direction:column + translateY(calc(-100% - var(--gap)))) · explicit viewport height clipping · 180deg mask fade · positioned center-band highlight (fake slot-machine focus without JS) · fixed row heights for steady rhythm · reduced-motion scrollable-list fallback

Make it yours

  • Column height: the 340px viewport — show 4–6 rows; taller columns need slower --speed to keep row dwell time constant.
  • Highlight band: move it off-center (e.g. top:30%) or delete it entirely for a plain roll.
  • Direction: animation-direction:reverse scrolls downward — upward reads 'progress', downward reads 'feed'.
  • Pair two columns at different speeds (one reversed) for a data-heavy hero.

Gotchas — read before shipping

  • A vertical marquee REQUIRES an explicit viewport height — without it the container grows to fit both groups and nothing clips or loops.
  • Uniform row heights matter: mixed heights make the center highlight band land on half a row.
  • gap must match in the keyframe calc, same as horizontal — vertical is not exempt from the seam math.
  • On short mobile viewports, cap the column with max-height:50vh so the marquee never dominates the screen.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by text-wrap as this demo is written. The core technique itself goes back further — Chrome all.

Identical support profile to the horizontal loops — flex-direction and translateY are ancient; mask fade is baseline 2023.

Techniques used in this demo

Search CodeFronts

Loading…