Raw winter
Twelve pieces cut in heavy fabrics. Scroll: the band speeds up with you.
Shop the dropEndless text bands that page scrolling pushes and tilts: they stop on hover, on keyboard focus or with the Pause button.
Updated
This effect is free — the Gallery category contains 44 effects total, including 6 free. Effect.Labs has 840 vanilla effects. Explore the category →
Twelve pieces cut in heavy fabrics. Scroll: the band speeds up with you.
Shop the dropBy two in the morning the presses have stopped. Only the cutting room stays lit, where three people finish an order due before dawn.
Nobody here calls it night work. They say “the second shift”, as if the day simply carried on under the neon lights.
Three days of music, forty concerts. Hover today's times to stop them and read.
Every .vmq-root inside a .fx-0831-root is one band. The script keeps your items (.vmq-item, .vmq-sep) and appends just enough copies to cover the band width; the position moves in pixels per second based on elapsed time (same speed on a 60 Hz or 120 Hz screen) and wraps after exactly one set of items, so there is no visible seam.
The base speed (50 px/s) breathes slightly (±12%). Scrolling the page adds a push proportional to the pixels scrolled: scrolling down speeds the band up, scrolling up slows it and can briefly reverse it. Horizontal mouse movement inside the root, between the bands, does the same (on a band, stopping wins). The gap from the base speed leans the text (skewX, 7° at most), then everything eases back.
Bands in the same root share their state: data-dir="rtl" reverses one band, hovering or focusing any band stops them all in about 0.4 s, and the .vmq-pause-btn button freezes them. Two roots on a page stay independent. Off screen the loop stops; it rebuilds when the root width changes.
aria-pressed and freezes every band of the root until the next click (WCAG 2.2.2).aria-hidden="true" and their links tabindex="-1", so nothing is announced or reached twice.prefers-reduced-motion: reduce, no copy is created and nothing moves; each band becomes a line you scroll with a finger, the wheel or the keyboard (it gets tabindex="0" when its content overflows). The Pause button and the demo hint are hidden.aria-hidden="true".Pointer Events, IntersectionObserver and ResizeObserver (both feature-tested), mask-image with the -webkit- prefix. Measured on 2026-11-08 in Chromium, Firefox and Playwright's WebKit: speed, pause on hover, focus and button, two independent copies, reduced motion, touch at 390 px.
Without JavaScript, each band shows your original items, still, clipped at the right edge. Without CSS masks, the band edges are sharp instead of fading out.
Copy the three blocks into your page. No dependencies.
<div class="fx-0831-root" data-vmq-speed="50" data-vmq-skew="7">
<div class="vmq-demo-wrapper">
<span class="vmq-demo-label" aria-hidden="true">move the cursor · scroll the page</span>
<!-- upper secondary band (RTL, small) -->
<div class="vmq-stripe vmq-stripe--sm">
<div class="vmq-root" data-dir="rtl"
style="--vmq-size:clamp(13px,2.4vw,20px);--vmq-gap:18px;--vmq-sep-gap:10px;">
<div class="vmq-track">
<span class="vmq-item" data-style="outline">SEAMLESS</span>
<span class="vmq-sep" aria-hidden="true">·</span>
<span class="vmq-item" data-style="outline">INFINITE</span>
<span class="vmq-sep" aria-hidden="true">·</span>
<span class="vmq-item" data-style="outline">VELOCITY</span>
<span class="vmq-sep" aria-hidden="true">·</span>
<span class="vmq-item" data-style="outline">MARQUEE</span>
<span class="vmq-sep" aria-hidden="true">·</span>
<span class="vmq-item" data-style="outline">NO GAP</span>
<span class="vmq-sep" aria-hidden="true">·</span>
<span class="vmq-item" data-style="outline">SCROLL PHYSICS</span>
<span class="vmq-sep" aria-hidden="true">·</span>
</div>
</div>
</div>
<!-- main band (large type, LTR) -->
<div class="vmq-stripe vmq-stripe--lg">
<div class="vmq-root" style="--vmq-size:clamp(30px,6vw,56px);--vmq-gap:24px;">
<div class="vmq-track">
<span class="vmq-item">NEW DROP</span>
<span class="vmq-sep" aria-hidden="true">✦</span>
<span class="vmq-item" data-style="outline">EFFECT.LABS</span>
<span class="vmq-sep" aria-hidden="true">✦</span>
<span class="vmq-item">VANILLA JS</span>
<span class="vmq-sep" aria-hidden="true">✦</span>
<span class="vmq-item" data-style="outline">NO FRAMEWORK</span>
<span class="vmq-sep" aria-hidden="true">✦</span>
<span class="vmq-item">OPEN SOURCE</span>
<span class="vmq-sep" aria-hidden="true">✦</span>
<span class="vmq-item" data-style="outline">CSS + JS</span>
<span class="vmq-sep" aria-hidden="true">✦</span>
</div>
</div>
</div>
<!-- lower secondary band (RTL, small) -->
<div class="vmq-stripe vmq-stripe--sm">
<div class="vmq-root" data-dir="rtl"
style="--vmq-size:clamp(13px,2.4vw,20px);--vmq-gap:18px;--vmq-sep-gap:10px;">
<div class="vmq-track">
<span class="vmq-item" data-style="outline">INERTIA</span>
<span class="vmq-sep" aria-hidden="true">·</span>
<span class="vmq-item" data-style="outline">SKEWX</span>
<span class="vmq-sep" aria-hidden="true">·</span>
<span class="vmq-item" data-style="outline">MASK-IMAGE</span>
<span class="vmq-sep" aria-hidden="true">·</span>
<span class="vmq-item" data-style="outline">RAF + LERP</span>
<span class="vmq-sep" aria-hidden="true">·</span>
<span class="vmq-item" data-style="outline">A11Y</span>
<span class="vmq-sep" aria-hidden="true">·</span>
<span class="vmq-item" data-style="outline">WCAG 2.2.2</span>
<span class="vmq-sep" aria-hidden="true">·</span>
</div>
</div>
</div>
<button class="vmq-pause-btn" type="button" aria-pressed="false">
<span aria-hidden="true">⏸</span>
<span>Pause</span>
</button>
</div>
</div>
.fx-0831-root,
.fx-0831-root *,
.fx-0831-root *::before,
.fx-0831-root *::after {
box-sizing: border-box
}
.fx-0831-root {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 300px;
overflow: hidden;
position: relative;
background: rgb(26, 26, 36);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.vmq-root {
position: relative;
overflow: hidden;
width: 100%;
cursor: default;
-webkit-mask-image: linear-gradient(to right,
transparent 0%,
#000 9%,
#000 91%,
transparent 100%);
mask-image: linear-gradient(to right,
transparent 0%,
#000 9%,
#000 91%,
transparent 100%);
}
@supports not ((mask-image: linear-gradient(to right, #000, #000)) or (-webkit-mask-image: linear-gradient(to right, #000, #000))) {
.vmq-root {
border-radius: 0;
}
}
.vmq-track {
display: flex;
align-items: center;
white-space: nowrap;
will-change: transform;
transform-origin: left center;
user-select: none;
-webkit-user-select: none;
}
.vmq-item {
flex-shrink: 0;
display: inline-flex;
align-items: center;
padding: 0 var(--vmq-gap, 28px);
font-size: var(--vmq-size, clamp(24px, 5vw, 48px));
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1;
color: var(--vmq-color, #f1f5f9);
white-space: nowrap;
text-decoration: none;
}
.vmq-item[data-style="outline"] {
color: transparent;
-webkit-text-stroke: 1.5px var(--vmq-stroke, rgba(241, 245, 249, 0.45));
text-stroke: 1.5px var(--vmq-stroke, rgba(241, 245, 249, 0.45));
}
.vmq-sep {
flex-shrink: 0;
padding: 0 var(--vmq-sep-gap, 16px);
font-size: calc(var(--vmq-size, clamp(24px, 5vw, 48px)) * 0.60);
color: var(--vmq-accent, #6366f1);
line-height: 1;
pointer-events: none;
white-space: nowrap;
}
.vmq-demo-wrapper {
position: relative;
width: 100%;
min-height: 300px;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
background: #06060e;
border-radius: 14px;
overflow: hidden;
gap: 0;
}
.vmq-demo-label {
position: absolute;
top: 16px;
left: 50%;
transform: translateX(-50%);
font-size: 11px;
color: rgba(148, 163, 184, 0.30);
letter-spacing: 0.10em;
text-transform: uppercase;
font-weight: 500;
pointer-events: none;
white-space: nowrap;
z-index: 2;
}
.vmq-stripe {
display: flex;
align-items: center;
flex: 1;
min-height: 60px;
}
.vmq-stripe--lg {
flex: 2;
min-height: 100px;
}
.vmq-stripe--sm {
flex: 1;
opacity: 0.6;
min-height: 56px;
}
.vmq-pause-btn {
position: absolute;
bottom: 14px;
right: 14px;
z-index: 10;
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(15, 15, 26, 0.80);
border: 1px solid rgba(99, 102, 241, 0.35);
color: #c7d2fe;
padding: 6px 12px 6px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
letter-spacing: 0.01em;
transition: background 0.15s, border-color 0.15s;
outline: 2px solid transparent;
outline-offset: 2px;
}
.vmq-pause-btn:hover {
background: rgba(30, 30, 50, 0.90);
border-color: rgba(99, 102, 241, 0.65);
}
.vmq-pause-btn:focus-visible {
outline-color: #818cf8;
}
.vmq-pause-btn[aria-pressed="true"] {
background: rgba(49, 46, 129, 0.85);
border-color: #6366f1;
color: #e0e7ff;
}
/* reduced motion: still band, scrollable by hand (finger, wheel, keyboard) */
.vmq-root.vmq-static {
overflow-x: auto;
-webkit-mask-image: none;
mask-image: none;
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.35) transparent;
}
.vmq-root.vmq-static .vmq-track {
transform: none;
user-select: text;
-webkit-user-select: text;
}
.vmq-root:focus-visible {
outline: 2px solid #818cf8;
outline-offset: -2px;
}
@media (prefers-reduced-motion: reduce) {
.vmq-pause-btn,
.vmq-demo-label {
display: none;
}
}
(function () {
/* fx-0831 — Velocity Marquee : one instance per .fx-0831-root */
'use strict';
var RM = window.matchMedia('(prefers-reduced-motion: reduce)');
var FOCUSABLE = 'a[href], button, input, select, textarea, [tabindex]';
function num(v, d, min, max) {
var n = parseFloat(v);
if (isNaN(n)) n = d;
return Math.max(min, Math.min(max, n));
}
/* smoothing independent of the screen refresh rate (k = factor tuned for 60 fps) */
function ease(k, dt) { return 1 - Math.pow(1 - k, dt * 60); }
function setupRoot(root) {
if (root.getAttribute('data-vmq-ready')) return;
root.setAttribute('data-vmq-ready', '1');
var BASE = num(root.getAttribute('data-vmq-speed'), 50, 5, 400); /* px/s */
var SKEW_MAX = num(root.getAttribute('data-vmq-skew'), 7, 0, 20); /* degrees */
var SCROLL_K = 3.6; /* px/s added per px of page scroll */
var MOUSE_K = 1.7; /* px/s added per px of horizontal mouse movement */
var SKEW_K = 0.0047; /* degrees of lean per px/s away from the base speed */
var BREATHE = 0.12; /* slow breathing: ±12% of the speed */
var btn = root.querySelector('.vmq-pause-btn');
var bands = [];
var vel = BASE, skew = 0, sBonus = 0, mBonus = 0, time = 0;
var paused = false, visible = true, reduced = RM.matches;
var raf = 0, lastT = 0, lastY = window.scrollY || 0, lastX = null;
Array.prototype.forEach.call(root.querySelectorAll('.vmq-root'), function (el) {
var track = el.querySelector('.vmq-track');
if (!track) return;
var b = {
el: el, track: track, orig: Array.prototype.slice.call(track.children),
setW: 0, pos: 0, rtl: el.getAttribute('data-dir') === 'rtl', hover: false, focus: false
};
el.addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') b.hover = true; });
el.addEventListener('pointerleave', function () { b.hover = false; });
el.addEventListener('focusin', function () { b.focus = true; });
el.addEventListener('focusout', function () { b.focus = false; });
bands.push(b);
});
if (!bands.length) return;
function held() {
for (var i = 0; i < bands.length; i++) if (bands[i].hover || bands[i].focus) return true;
return false;
}
function clone(node) {
var c = node.cloneNode(true);
if (c.nodeType !== 1) return c;
c.setAttribute('aria-hidden', 'true');
c.removeAttribute('id');
if (c.matches(FOCUSABLE)) c.setAttribute('tabindex', '-1');
Array.prototype.forEach.call(c.querySelectorAll('[id]'), function (n) { n.removeAttribute('id'); });
Array.prototype.forEach.call(c.querySelectorAll(FOCUSABLE), function (n) { n.setAttribute('tabindex', '-1'); });
return c;
}
/* (re)builds each band: original set + copies hidden from screen readers */
function build() {
bands.forEach(function (b) {
var t = b.track;
while (t.lastChild && b.orig.indexOf(t.lastChild) < 0) t.removeChild(t.lastChild);
t.style.transform = '';
b.el.classList.toggle('vmq-static', reduced);
b.el.removeAttribute('tabindex');
if (reduced) {
b.setW = 0;
if (b.el.scrollWidth > b.el.clientWidth + 1) b.el.setAttribute('tabindex', '0');
return;
}
var cw = b.el.clientWidth || 480;
var sw = t.scrollWidth || 200;
var n = Math.ceil(cw / sw) + 1;
for (var i = 0; i < n; i++) b.orig.forEach(function (o) { t.appendChild(clone(o)); });
var first = b.orig[0], twin = t.children[b.orig.length];
var w = (first && twin) ? twin.getBoundingClientRect().left - first.getBoundingClientRect().left : 0;
b.setW = w > 1 ? w : sw;
b.pos = b.pos % b.setW;
});
if (!reduced) draw();
run();
}
function draw() {
bands.forEach(function (b) {
if (!b.setW) return;
var sk = b.rtl ? -skew : skew;
b.track.style.transform = 'translateX(' + (-b.pos).toFixed(2) + 'px) skewX(' + sk.toFixed(3) + 'deg)';
});
}
function frame(t) {
raf = 0;
if (reduced || paused || !visible) { lastT = 0; return; }
raf = requestAnimationFrame(frame);
var dt = lastT ? Math.min((t - lastT) / 1000, 0.05) : 0;
lastT = t;
if (!dt) return;
var hold = held();
time += 0.54 * dt;
var target = hold ? 0 : BASE * (1 + Math.sin(time) * BREATHE) + sBonus + mBonus;
sBonus *= Math.pow(0.80, dt * 60);
mBonus *= Math.pow(0.84, dt * 60);
/* hover or focus: quick stop (WCAG 2.2.2), otherwise smooth return */
vel += (target - vel) * ease(hold ? 0.16 : 0.07, dt);
if (hold && Math.abs(vel) < 0.5) vel = 0;
var tSkew = hold ? 0 : Math.max(-SKEW_MAX, Math.min(SKEW_MAX, -(vel - BASE) * SKEW_K));
skew += (tSkew - skew) * ease(0.09, dt);
bands.forEach(function (b) {
if (!b.setW) return;
var step = (b.rtl ? -vel : vel) * dt;
b.pos = ((b.pos + step) % b.setW + b.setW) % b.setW;
});
draw();
}
function run() {
if (!raf && !reduced && !paused && visible) { lastT = 0; raf = requestAnimationFrame(frame); }
}
window.addEventListener('scroll', function () {
var y = window.scrollY || 0;
if (visible) sBonus = (y - lastY) * SCROLL_K;
lastY = y;
}, { passive: true });
root.addEventListener('pointermove', function (e) {
if (e.pointerType !== 'mouse') return;
if (lastX !== null) mBonus = (e.clientX - lastX) * MOUSE_K;
lastX = e.clientX;
});
root.addEventListener('pointerleave', function () { lastX = null; });
if (btn) {
btn.addEventListener('click', function () {
paused = !paused;
btn.setAttribute('aria-pressed', paused ? 'true' : 'false');
var icon = btn.querySelector('[aria-hidden="true"]');
if (icon) icon.textContent = paused ? '▶' : '⏸';
if (paused) { vel = 0; skew = 0; draw(); } else { vel = 0; run(); }
});
}
if (RM.addEventListener) {
RM.addEventListener('change', function (e) { reduced = e.matches; skew = 0; build(); });
}
if ('IntersectionObserver' in window) {
new IntersectionObserver(function (en) {
visible = en[0].isIntersecting;
run();
}).observe(root);
}
var lastW = root.clientWidth, pending = 0;
if ('ResizeObserver' in window) {
new ResizeObserver(function () {
if (!root.clientWidth || root.clientWidth === lastW || pending) return;
pending = requestAnimationFrame(function () { pending = 0; lastW = root.clientWidth; build(); });
}).observe(root);
}
build();
if (document.fonts && document.fonts.ready) document.fonts.ready.then(build);
}
function init() {
Array.prototype.forEach.call(document.querySelectorAll('.fx-0831-root'), setupRoot);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Speed (HTML — data-vmq-speed on .fx-0831-root) |
50 (px/s) | Cruising speed of every band in the root, from 5 to 400 px/s. Page scrolling adds to this value. |
Maximum lean (HTML — data-vmq-skew) |
7 (degrees) | Largest skewX angle taken when the band speeds up or slows down. 0 removes the lean. |
Band direction (HTML — data-dir="rtl" on .vmq-root) |
Absent: moves left | The band moves right; the scroll push is reversed too. |
Content (HTML — children of .vmq-track) |
Six words and six separators per band | Use your own .vmq-item: text, <a class="vmq-item" href="…">, an <img> logo inside an item. data-style="outline" shows a word as an outline. |
Size and spacing (CSS — --vmq-size, --vmq-gap, --vmq-sep-gap on .vmq-root) |
clamp(24px, 5vw, 48px), 28 px, 16 px | Text size, space on each side of a word and around separators. |
Colors (CSS — --vmq-color, --vmq-stroke, --vmq-accent) |
#f1f5f9, rgba(241,245,249,.45), #6366f1 | Solid text, outline of outline words and separators; set them on the root or on one band. |
Pause button (HTML — .vmq-pause-btn) |
Present | You may remove it, but then keep another way for keyboard users to stop the motion (WCAG 2.2.2). |
Scrolling up removes speed: a quick flick goes past the base speed and the band backs up for a moment, then the push fades (it loses about 20% per frame) and the band returns to its normal direction.
Yes: every child of .vmq-track is kept. Links stay clickable, keyboard focus stops the band, and the copies made for the loop are removed from the keyboard order and from screen readers. Give images a fixed height so the width measured at start is right.
No. The script rebuilds the bands once document.fonts.ready resolves and whenever the root width changes, so one lap always matches the words actually displayed.