Hiver brut
Douze pièces coupées dans des tissus lourds. Faites défiler : la bande accélère avec vous.
Voir le dropBandes de texte sans fin que le défilement de la page pousse et incline : elles s'arrêtent au survol, au focus ou d'un clic sur Pause.
Mis à jour le
Cet effet est gratuit — la catégorie Galerie en contient 44 au total, dont 6 gratuits. Effect.Labs regroupe 840 effets vanilla. Découvrir la catégorie →
Douze pièces coupées dans des tissus lourds. Faites défiler : la bande accélère avec vous.
Voir le dropÀ deux heures du matin, les presses se sont tues. Seul l'atelier de coupe reste éclairé, où trois personnes terminent une commande attendue avant l'aube.
Personne ici ne parle de travail de nuit. On dit « la deuxième équipe », comme si la journée continuait simplement sous les néons.
Trois jours de musique, quarante concerts. Survolez les horaires du jour pour les arrêter et les lire.
Chaque .vmq-root placé dans une racine .fx-0831-root est une bande. Le script garde vos éléments (.vmq-item, .vmq-sep) puis ajoute derrière eux juste assez de copies pour couvrir la largeur de la bande ; la position avance en pixels par seconde, calculée sur le temps écoulé (même vitesse sur un écran 60 Hz ou 120 Hz), et revient à zéro après exactement un jeu d'éléments, d'où l'absence de raccord visible.
La vitesse de base (50 px/s) respire doucement (±12 %). Faire défiler la page ajoute une impulsion proportionnelle au nombre de pixels défilés : vers le bas la bande accélère, vers le haut elle ralentit et peut repartir à l'envers. Un mouvement horizontal de la souris dans la racine, entre les bandes, fait de même (sur une bande, c'est l'arrêt qui l'emporte). L'écart à la vitesse de base incline le texte (skewX, 7° au maximum) puis tout revient au calme en douceur.
Les bandes d'une même racine partagent leur état : data-dir="rtl" inverse le sens d'une bande, le survol ou le focus d'une bande les arrête toutes en 0,4 s environ, et le bouton .vmq-pause-btn les fige. Deux racines dans la page restent indépendantes. Hors de l'écran, la boucle s'arrête ; elle se reconstruit quand la largeur de la racine change.
aria-pressed et fige toutes les bandes de la racine jusqu'au clic suivant (WCAG 2.2.2).aria-hidden="true" et leurs liens tabindex="-1", donc rien n'est annoncé deux fois ni atteint deux fois au clavier.prefers-reduced-motion: reduce, aucune copie n'est créée et rien ne bouge ; chaque bande devient une ligne défilable au doigt, à la molette ou au clavier (elle reçoit tabindex="0" quand son contenu dépasse). Le bouton Pause et la consigne de démonstration sont alors masqués.aria-hidden="true".Pointer Events, IntersectionObserver et ResizeObserver (ces deux derniers testés avant usage), mask-image avec préfixe -webkit-. Mesuré le 08/11/2026 dans Chromium, Firefox et le WebKit de Playwright : vitesse, pause au survol, au focus et au bouton, deux copies indépendantes, mouvement réduit, toucher à 390 px.
Sans JavaScript, chaque bande affiche vos éléments d'origine, immobiles, coupés au bord droit. Sans masque CSS, les bords de la bande sont nets au lieu de s'estomper.
Copiez les trois blocs dans votre page. Aucune dépendance.
<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">déplacez le curseur · scrollez la page</span>
<!-- bande secondaire haute (RTL, petite) -->
<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>
<!-- bande principale (grande typo, 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>
<!-- bande secondaire basse (RTL, petite) -->
<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;
}
/* mouvement réduit : bande immobile, défilable à la main (doigt, molette, clavier) */
.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 : une instance par .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));
}
/* lissage indépendant de la fréquence d'écran (k = facteur prévu pour 60 im/s) */
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); /* degrés */
var SCROLL_K = 3.6; /* px/s ajoutés par px de défilement de la page */
var MOUSE_K = 1.7; /* px/s ajoutés par px de mouvement horizontal de la souris */
var SKEW_K = 0.0047; /* degrés d'inclinaison par px/s d'écart à la vitesse de base */
var BREATHE = 0.12; /* respiration lente : ±12 % de la vitesse */
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)construit chaque bande : jeu d'origine + copies cachées aux lecteurs d'écran */
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);
/* survol ou focus : arrêt rapide (WCAG 2.2.2), sinon retour en douceur */
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 passées à l'API ou attributs data-* :
| Option / propriété | Défaut | Effet |
|---|---|---|
Vitesse (HTML — data-vmq-speed sur .fx-0831-root) |
50 (px/s) | Vitesse de croisière de toutes les bandes de la racine, de 5 à 400 px/s. Le défilement de la page s'ajoute à cette valeur. |
Inclinaison maximale (HTML — data-vmq-skew) |
7 (degrés) | Angle maximal du skewX pris quand la bande accélère ou freine. 0 supprime l'inclinaison. |
Sens d'une bande (HTML — data-dir="rtl" sur .vmq-root) |
Absent : défilement vers la gauche | La bande part vers la droite ; l'impulsion du défilement est inversée elle aussi. |
Contenu (HTML — enfants de .vmq-track) |
Six mots et six séparateurs par bande | Mettez vos propres .vmq-item : texte, <a class="vmq-item" href="…">, logo <img> dans un élément. data-style="outline" affiche un mot en contour. |
Taille et espacements (CSS — --vmq-size, --vmq-gap, --vmq-sep-gap sur .vmq-root) |
clamp(24px, 5vw, 48px), 28 px, 16 px | Taille du texte, marge de part et d'autre de chaque mot et autour des séparateurs. |
Couleurs (CSS — --vmq-color, --vmq-stroke, --vmq-accent) |
#f1f5f9, rgba(241,245,249,.45), #6366f1 | Texte plein, contour des mots outline et séparateurs ; à poser sur la racine ou sur une bande. |
Bouton Pause (HTML — .vmq-pause-btn) |
Présent | Vous pouvez le retirer, mais gardez alors un autre moyen d'arrêter le mouvement pour les personnes au clavier (WCAG 2.2.2). |
Faire défiler la page vers le haut retire de la vitesse : un geste rapide dépasse la vitesse de base et la bande recule un instant, puis l'impulsion s'éteint (elle perd environ 20 % par image) et la bande reprend son sens normal.
Oui : tout enfant de .vmq-track est repris. Les liens restent cliquables, le focus clavier arrête la bande, et les copies créées pour la boucle sont retirées de l'ordre du clavier et des lecteurs d'écran. Pour des images, donnez-leur une hauteur fixe pour que la largeur mesurée au départ soit juste.
Non. Le script reconstruit les bandes quand document.fonts.ready est résolu et à chaque changement de largeur de la racine, pour que la longueur d'un tour corresponde aux mots réellement affichés.