Today
Last entry 45 minutes before closing
- Main pool · 25 m7:00 – 20:30
- Play pool10:00 – 18:00
- Nordic poolClosed
- Paddling pool10:00 – 18:00
A line of messages that scrolls right to left at a steady speed, endless and gap-free at any width, and stops on hover or with a click on Pause.
Updated
This effect is free — the Gallery category contains 44 effects total, including 6 free. Effect.Labs has 840 vanilla effects. Explore the category →
Last entry 45 minutes before closing
Craft lemonade
Real fruit, cane sugar, nothing else.
33 cl returnable bottleCrates of 12Delivered within 48 h
New video
Taken from the album out on 6 March. Tour across France this spring.
You write your messages once, each in an <li> of the .bmq-group list. On load, the script measures that list and the .bmq-viewport window, then appends as many copies as it takes to cover the window plus one: one copy on a phone, two in a 1,900 px area with the demo text (about 1,240 px long). The old version doubled the text once and for all and left an empty stretch on the right as soon as the area was wider than the text.
The bmq-scroll CSS animation moves the track by exactly one list length, then starts over: the next copy has taken the original's place to the pixel, so the seam never shows. The script derives the duration from data-speed (85 px per second by default): a text twice as long takes twice as long, it does not rush through twice as fast. data-direction="right" reverses the direction.
A ResizeObserver recounts when the area or the text changes size (phone rotation, font loaded late), and a MutationObserver rebuilds the copies if a script edits the list. Each band is handled on its own: several bands on one page each keep their speed and their button.
aria-pressed) stops and restarts the band with a mouse, a finger or the keyboard; hovering the band also stops it while you read. A band that moves for more than 5 seconds must be stoppable (WCAG 2.2.2).aria-hidden="true". Measured before the repair: every message was read twice.tabindex="-1"). As soon as a link has focus, the band returns to its start, stays still and brings that link fully into view; it moves again once focus leaves.prefers-reduced-motion: reduce, nothing scrolls by itself, the copies and the button go away, and the line can be scrolled by hand (finger, trackpad, Shift + wheel, thin bar) to read the end; when it overflows, it also takes keyboard focus (Tab) and scrolls with the arrow keys. Without JavaScript, it stays still and scrollable by hand.--bmq-color.CSS and 4.5 KB of commented JavaScript, no dependency. A CSS animation whose offset comes from a variable (var(--bmq-shift) inside @keyframes), ResizeObserver (Chrome 64, Firefox 69, Safari 13.1), MutationObserver, prefers-reduced-motion (Chrome 74, Firefox 63, Safari 10.1). Measured in Chromium 151, Firefox 153 and WebKit 26.5: no gap at 390, 800 and 1,900 px, two independent bands, script pasted twice without extra copies, 0 errors.
Without JavaScript, the list stays still, shown once and scrollable by hand. Without ResizeObserver (Safari before 13.1), copies are only recounted when the window is resized. The thin scrollbar of reduced-motion mode (scrollbar-width) only exists in Chrome 121+, Firefox 64+ and Safari 18.2+; elsewhere the regular bar shows.
Copy the three blocks into your page. No dependencies.
<div class="fx-0187-root" data-speed="85" data-direction="left">
<div class="bmq-band">
<div class="bmq-viewport">
<div class="bmq-track">
<ul class="bmq-group" role="list">
<li>Welcome to Effect.Labs</li>
<li>Premium Web Effects</li>
<li>Modern CSS Animations</li>
<li>Infinite Scrolling</li>
</ul>
</div>
</div>
<button type="button" class="bmq-toggle" aria-pressed="false" aria-label="Pause"></button>
</div>
</div>
.fx-0187-root,
.fx-0187-root *,
.fx-0187-root *::before,
.fx-0187-root *::after {
box-sizing: border-box;
}
/* Settings: variables below; speed (px/s) and direction in the data-speed and data-direction attributes of the HTML */
.fx-0187-root {
--bmq-height: 250px; /* frame height (auto: the band height) */
--bmq-bg: rgb(26, 26, 36); /* frame background */
--bmq-color: #6366f1; /* text colour */
--bmq-size: 1.5rem; /* text size */
--bmq-space: 4rem; /* space between two items */
--bmq-button: rgba(255, 255, 255, .8); /* Pause button colour */
--bmq-padding: 1rem 0; /* frame inner padding */
--bmq-button-size: 2rem; /* Pause button diameter */
display: flex;
align-items: center;
width: 100%;
min-height: var(--bmq-height);
padding: var(--bmq-padding);
overflow: hidden;
position: relative;
background: var(--bmq-bg);
color: #fff;
font-family: sans-serif;
}
.bmq-band {
display: flex;
align-items: center;
gap: .75rem;
width: 100%;
min-width: 0;
}
.bmq-viewport {
flex: 1;
min-width: 0;
overflow: hidden;
}
.bmq-track {
display: flex;
width: max-content;
}
.bmq-group {
display: flex;
align-items: center;
flex-shrink: 0;
margin: 0;
padding: 0;
list-style: none;
font-size: var(--bmq-size);
font-weight: 600;
color: var(--bmq-color);
}
.bmq-group > li {
margin: 0 calc(var(--bmq-space) / 2);
white-space: nowrap;
}
/* Scrolling: started by the script once the track is filled (bmq-on class) */
.fx-0187-root.bmq-on .bmq-track {
animation-name: bmq-scroll;
animation-duration: var(--bmq-duration, 15s);
animation-timing-function: linear;
animation-iteration-count: infinite;
will-change: transform;
}
.fx-0187-root[data-direction="right"] .bmq-track {
animation-direction: reverse;
}
@keyframes bmq-scroll {
from {
transform: translateX(0);
}
to {
transform: translateX(var(--bmq-shift));
}
}
/* Pause: hovering the band or the Pause button */
.bmq-viewport:hover .bmq-track,
.fx-0187-root.bmq-paused .bmq-track {
animation-play-state: paused;
}
/* Keyboard focus inside the band (bmq-focus class): band back to its start and still, the browser scrolls to the link */
.fx-0187-root.bmq-focus .bmq-track {
animation: none;
}
/* Pause / Play button (remove it from the HTML if the band is short and purely decorative) */
.bmq-toggle {
flex: none;
position: relative;
width: var(--bmq-button-size);
height: var(--bmq-button-size);
margin: 0 1rem 0 0;
padding: 0;
border: 1px solid currentColor;
border-radius: 50%;
background: transparent;
color: var(--bmq-button);
cursor: pointer;
}
.bmq-toggle::before,
.bmq-toggle::after {
content: "";
position: absolute;
top: 50%;
width: 3px;
height: 10px;
margin-top: -5px;
border-radius: 1px;
background: currentColor;
}
.bmq-toggle::before {
left: calc(50% - 4px);
}
.bmq-toggle::after {
left: calc(50% + 1px);
}
.bmq-toggle[aria-pressed="true"]::before {
left: calc(50% - 3px);
width: 0;
height: 0;
margin-top: -6px;
border-style: solid;
border-width: 6px 0 6px 10px;
border-color: transparent transparent transparent currentColor;
border-radius: 0;
background: none;
}
.bmq-toggle[aria-pressed="true"]::after {
display: none;
}
.bmq-toggle:hover {
background: rgba(127, 127, 127, .18);
}
.bmq-toggle:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
/* Without the script: still band, scrollable by hand */
.fx-0187-root:not(.bmq-on) .bmq-viewport {
overflow-x: auto;
}
.fx-0187-root:not(.bmq-on) .bmq-toggle {
display: none;
}
/* Reduced motion: nothing moves by itself, the content stays readable and scrollable by hand */
@media (prefers-reduced-motion: reduce) {
.fx-0187-root.bmq-on .bmq-track {
animation: none;
}
.fx-0187-root .bmq-viewport {
overflow-x: auto;
scrollbar-width: thin;
}
.fx-0187-root .bmq-viewport:focus-visible {
outline: 2px solid currentColor;
outline-offset: -2px;
}
.fx-0187-root .bmq-clone,
.fx-0187-root .bmq-toggle {
display: none;
}
}
(function () {
'use strict';
// fx-0187 — Basic Marquee : the content is written once, the script copies it to fill the band
var ROOT = '.fx-0187-root';
var VERTICAL = false;
var DEFAULT_SPEED = 85; // pixels per second
var FOCUSABLE = 'a[href], button, input, select, textarea, [tabindex]';
// Decorative copy: hidden from screen readers and out of the keyboard order
function copyOf(group) {
var copy = group.cloneNode(true);
copy.classList.add('bmq-clone');
copy.setAttribute('aria-hidden', 'true');
copy.removeAttribute('id');
copy.querySelectorAll('[id]').forEach(function (el) { el.removeAttribute('id'); });
copy.querySelectorAll(FOCUSABLE).forEach(function (el) { el.setAttribute('tabindex', '-1'); });
return copy;
}
function setup(root) {
if (root.bmqReady) return; // script pasted twice
root.bmqReady = true;
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
var bands = [];
root.querySelectorAll('.bmq-viewport').forEach(function (viewport) {
var track = viewport.querySelector('.bmq-track');
var group = track && track.querySelector('.bmq-group');
if (!group) return;
var holder = viewport.closest('[data-speed]');
bands.push({ viewport: viewport, track: track, group: group, holder: holder && root.contains(holder) ? holder : root });
});
if (!bands.length) return;
// Enough copies to cover the visible area + one coming in; shift = one group
function fill(band) {
var box = band.group.getBoundingClientRect();
var size = VERTICAL ? box.height : box.width;
var room = VERTICAL ? band.viewport.clientHeight : band.viewport.clientWidth;
if (!size) return;
var copies = band.track.children.length;
var needed = Math.ceil(room / size) + 1;
for (; copies < needed; copies++) band.track.appendChild(copyOf(band.group));
var speed = parseFloat(band.holder.getAttribute('data-speed')) || DEFAULT_SPEED;
band.track.style.setProperty('--bmq-shift', -size + 'px');
band.track.style.setProperty('--bmq-duration', (size / speed) + 's');
// Reduced motion: the band scrolls by hand, so also with the keyboard (tabindex) when it overflows
var over = VERTICAL ? band.viewport.scrollHeight > band.viewport.clientHeight + 1 : band.viewport.scrollWidth > band.viewport.clientWidth + 1;
if (reduced.matches && over) band.viewport.setAttribute('tabindex', '0');
else band.viewport.removeAttribute('tabindex');
root.classList.add('bmq-on');
}
function fillAll() { bands.forEach(fill); }
fillAll();
// Content changed later (translated text, new item): the copies are rebuilt
if ('MutationObserver' in window) {
bands.forEach(function (band) {
new MutationObserver(function () {
band.track.querySelectorAll('.bmq-clone').forEach(function (copy) { copy.remove(); });
fill(band);
}).observe(band.group, { childList: true, subtree: true, characterData: true });
});
}
if ('ResizeObserver' in window) {
var observer = new ResizeObserver(fillAll);
bands.forEach(function (band) {
observer.observe(band.viewport);
observer.observe(band.group);
});
} else {
window.addEventListener('resize', fillAll);
}
// Keyboard focus on a link in the band: still band (bmq-focus) with the link brought into view, put back when focus leaves
root.addEventListener('focusin', function (event) {
var viewport = event.target.closest('.bmq-viewport');
root.classList.toggle('bmq-focus', !!viewport);
if (!viewport) return;
var view = viewport.getBoundingClientRect();
var item = event.target.getBoundingClientRect();
if (VERTICAL) {
if (item.bottom > view.bottom) viewport.scrollTop += item.bottom - view.bottom;
if (item.top < view.top) viewport.scrollTop -= view.top - item.top;
} else {
if (item.right > view.right) viewport.scrollLeft += item.right - view.right;
if (item.left < view.left) viewport.scrollLeft -= view.left - item.left;
}
});
root.addEventListener('focusout', function () {
root.classList.remove('bmq-focus');
if (!reduced.matches) bands.forEach(function (band) { band.viewport.scrollLeft = 0; band.viewport.scrollTop = 0; });
});
var toggle = root.querySelector('.bmq-toggle');
if (toggle) {
toggle.addEventListener('click', function () {
var paused = toggle.getAttribute('aria-pressed') !== 'true';
toggle.setAttribute('aria-pressed', paused ? 'true' : 'false');
root.classList.toggle('bmq-paused', paused);
});
}
}
function init() {
document.querySelectorAll(ROOT).forEach(setup);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Content (HTML — the <li> of .bmq-group) |
4 demo texts | Write each message once: text, links, icons or images. The script makes the copies, and rebuilds them if the content changes later. |
data-speed (on the root) |
85 | Speed in pixels per second, the same whatever the text length. 30 to 50 for sentences to read, 80 to 120 for decoration. |
data-direction |
left | right scrolls from left to right. |
--bmq-color / --bmq-size |
#6366f1 / 1.5rem | Colour and size of the band text. |
--bmq-space |
4rem | Gap between two messages, the same at the seam between two copies. |
--bmq-height / --bmq-padding / --bmq-bg |
250px / 1rem 0 / rgb(26, 26, 36) | Demo frame. For a band placed inside a page: auto, 0 and transparent. |
--bmq-button / --bmq-button-size |
rgba(255, 255, 255, .8) / 2rem | Colour and diameter of the Pause button. Only remove the <button> from the HTML for a short, decorative band. |
The text used to be written twice in the HTML and the animation shifted it by 50%: if the area was wider than the text (about 1,240 px here), the end of the second copy arrived before the right edge. The script now counts copies from the real width, and recounts on every resize.
Add an <li> to the .bmq-group list (with append(), for instance): the script notices, removes the old copies, makes new ones and recomputes the duration to keep the same speed.
The Pause button is the reliable way. Measured in Chromium in touch mode: a tap on the band also stops it, because the browser simulates a hover until the next tap elsewhere on the page; not every mobile browser does this. Swiping vertically over the band always scrolls the page, and tapping a link in the band opens it, as anywhere else.