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
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 & 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 & 3D</span>
<span class="mqs-10__row">Design systems</span>
<span class="mqs-10__row">Development</span>
</div>
</div>
</div>
</div>
</section><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 & 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 & 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;
}
}.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. */
/* No JavaScript — the horizontal pattern rotated: flex-direction:column, an explicit viewport height, translateY(calc(-100% - var(--gap))), and a 180deg mask. */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 & 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 & 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. */
```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 & 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 & 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
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 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.