CSS Marquee
A lightweight looping marquee powered by a single CSS keyframe animation. JavaScript optionally duplicates the list for a seamless loop, calculates speed from element width, and pauses animation when the marquee is off-screen.
Code
<div data-css-marquee="" class="marquee-css">
<div data-css-marquee-list="" class="marquee-css__list">
<div class="marquee-css__item">
<p class="marquee-css__item-p">CSS Marquee</p>
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 50 50" fill="none" class="marquee-css__item-svg"><path d="M17.6777 32.3223C12.9893 27.6339 6.63041 25 0 25C6.63041 25 12.9893 22.3661 17.6777 17.6777C22.3661 12.9893 25 6.63041 25 0C25 6.63041 27.6339 12.9893 32.3223 17.6777C37.0107 22.3661 43.3696 25 50 25C43.3696 25 37.0107 27.6339 32.3223 32.3223C27.6339 37.0107 25 43.3696 25 50C25 43.3696 22.3661 37.0107 17.6777 32.3223Z" fill="#C9FC7D"></path></svg>
</div>
<div class="marquee-css__item">
<p class="marquee-css__item-p">CSS Marquee</p>
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 50 50" fill="none" class="marquee-css__item-svg"><path d="M17.6777 32.3223C12.9893 27.6339 6.63041 25 0 25C6.63041 25 12.9893 22.3661 17.6777 17.6777C22.3661 12.9893 25 6.63041 25 0C25 6.63041 27.6339 12.9893 32.3223 17.6777C37.0107 22.3661 43.3696 25 50 25C43.3696 25 37.0107 27.6339 32.3223 32.3223C27.6339 37.0107 25 43.3696 25 50C25 43.3696 22.3661 37.0107 17.6777 32.3223Z" fill="#C9FC7D"></path></svg>
</div>
<div class="marquee-css__item">
<p class="marquee-css__item-p">CSS Marquee</p>
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 50 50" fill="none" class="marquee-css__item-svg"><path d="M17.6777 32.3223C12.9893 27.6339 6.63041 25 0 25C6.63041 25 12.9893 22.3661 17.6777 17.6777C22.3661 12.9893 25 6.63041 25 0C25 6.63041 27.6339 12.9893 32.3223 17.6777C37.0107 22.3661 43.3696 25 50 25C43.3696 25 37.0107 27.6339 32.3223 32.3223C27.6339 37.0107 25 43.3696 25 50C25 43.3696 22.3661 37.0107 17.6777 32.3223Z" fill="#C9FC7D"></path></svg>
</div>
<div class="marquee-css__item">
<p class="marquee-css__item-p">CSS Marquee</p>
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 50 50" fill="none" class="marquee-css__item-svg"><path d="M17.6777 32.3223C12.9893 27.6339 6.63041 25 0 25C6.63041 25 12.9893 22.3661 17.6777 17.6777C22.3661 12.9893 25 6.63041 25 0C25 6.63041 27.6339 12.9893 32.3223 17.6777C37.0107 22.3661 43.3696 25 50 25C43.3696 25 37.0107 27.6339 32.3223 32.3223C27.6339 37.0107 25 43.3696 25 50C25 43.3696 22.3661 37.0107 17.6777 32.3223Z" fill="#C9FC7D"></path></svg>
</div>
</div>
</div>.marquee-css {
color: #efeeec;
background-color: #000;
width: 100%;
max-width: 42em;
display: flex;
position: relative;
overflow: hidden;
}
.marquee-css__list {
flex: none;
align-items: center;
display: flex;
position: relative;
}
.marquee-css__item {
grid-column-gap: 1em;
grid-row-gap: 1em;
flex: 0;
align-items: center;
padding-top: 1em;
padding-bottom: 1em;
padding-right: 1em;
display: flex;
}
.marquee-css__item-p {
white-space: nowrap;
margin-bottom: 0;
font-size: 1.5em;
line-height: 1;
}
.marquee-css__item-svg {
width: 1em;
}
/* CSS Keyframe Animation */
@keyframes translateX {
to {
transform: translateX(-100%);
}
}
[data-css-marquee-list] {
animation: translateX 30s linear;
animation-iteration-count: infinite;
animation-play-state: paused;
}// Note: The Javascript is optional. Read the documentation for the CSS-only version.
function initCSSMarquee() {
const pixelsPerSecond = 75; // Set the marquee speed (pixels per second)
const marquees = document.querySelectorAll('[data-css-marquee]');
// Duplicate each [data-css-marquee-list] element inside its container
marquees.forEach(marquee => {
marquee.querySelectorAll('[data-css-marquee-list]').forEach(list => {
const duplicate = list.cloneNode(true);
marquee.appendChild(duplicate);
});
});
// Create an IntersectionObserver to check if the marquee container is in view
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
entry.target.querySelectorAll('[data-css-marquee-list]').forEach(list =>
list.style.animationPlayState = entry.isIntersecting ? 'running' : 'paused'
);
});
}, { threshold: 0 });
// Calculate the width and set the animation duration accordingly
marquees.forEach(marquee => {
marquee.querySelectorAll('[data-css-marquee-list]').forEach(list => {
list.style.animationDuration = (list.offsetWidth / pixelsPerSecond) + 's';
list.style.animationPlayState = 'paused';
});
observer.observe(marquee);
});
}
// Initialize CSS Marquee
document.addEventListener('DOMContentLoaded', function() {
initCSSMarquee();
});Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| data-css-marquee | string | "" | Add to the outermost marquee container. The script queries this element to find lists to duplicate, calculate animation duration, and observe viewport intersection. |
| data-css-marquee-list | string | "" | Add to the moving list element. The script clones this element and appends the clone to the marquee container to create the seamless loop. The CSS keyframe animation is applied to this attribute selector. |
Notes
- •The JavaScript is optional — see the CSS-only version in the guide below for a no-JS implementation.
- •The script calculates animation-duration from the list's offsetWidth divided by pixelsPerSecond, so the speed remains consistent regardless of how many items are in the list.
- •The duplicated list is appended after the original so both scroll in sync under the single @keyframes translateX animation — no JS animation is used.
- •IntersectionObserver pauses animation-play-state when the marquee container is off-screen, saving CPU on long pages without needing GSAP or ScrollTrigger.
- •Items must not wrap (white-space: nowrap on text, flex: none on the list) otherwise the loop point will be visible.
- •Only one list needs to be in the HTML — the script clones it. For the CSS-only version you must duplicate the list manually in HTML.
Guide
JS-powered version
Add [data-css-marquee] to the wrapper and [data-css-marquee-list] to the single list of items. Include the JavaScript — it duplicates the list, calculates duration from element width, and pauses animation when off-screen.
Speed
Change pixelsPerSecond in the JS to control the marquee speed. A higher value moves the marquee faster; a lower value slows it down. The duration is recalculated automatically from the list's measured width.
const pixelsPerSecond = 75; // increase for faster, decrease for slowerCSS-only version
To use without JavaScript: manually duplicate the [data-css-marquee-list] element in HTML, remove the animation-play-state: paused line from the CSS, and tune the animation duration in seconds until the loop looks seamless.
@keyframes translateX {
to { transform: translateX(-100%); }
}
[data-css-marquee-list] {
animation: translateX 30s linear; /* tune this number */
animation-iteration-count: infinite;
/* Remove: animation-play-state: paused; */
}Adding items
Add as many .marquee-css__item elements as needed inside the single [data-css-marquee-list]. The script measures the total width after the DOM is ready and sets the duration accordingly — no manual timing adjustments needed.
Viewport pause
The IntersectionObserver watches the [data-css-marquee] wrapper with threshold: 0, meaning animation pauses as soon as even one pixel leaves the viewport, and resumes the moment any part re-enters.