Infinite Marquee Text Scroll
A seamless, infinite scrolling text marquee component. Features duplicate text layers for a continuous loop, pause on hover, variable speed controls, bi-directional support, and elegant gradient fade edges.

Create a captivating, seamless scrolling text effect perfect for announcements, partners lists, or news tickers. This snippet uses pure CSS for the animation and gradient masks, ensuring high performance and smooth motion.
Features
- Seamless Looping: Uses duplicate content to create a perfect infinite loop.
- Directional Control: Support for Left-to-Right and Right-to-Left scrolling.
- Interactive: Pauses smoothly on hover.
- Aesthetic Polish: Gradient fade masks on the edges for a premium look.
- Customizable: Easy control over speed, gap, and direction via CSS variables.
HTML Structure
The structure requires a wrapper for the mask/overflow and a track for the animation. Inside the track, place your content twice (or more) to ensure there is no gap when the animation resets.
<div class="marquee-container">
<div class="marquee-track">
<!-- Content Group 1 -->
<div class="marquee-content">
<span>BREAKING NEWS</span>
<span class="separator">•</span>
<span>EXCLUSIVE OFFERS</span>
<span class="separator">•</span>
<span>LATEST UPDATES</span>
<span class="separator">•</span>
<span>SIGN UP NOW</span>
<span class="separator">•</span>
</div>
<!-- Content Group 2 (Duplicate of Group 1) -->
<div class="marquee-content" aria-hidden="true">
<span>BREAKING NEWS</span>
<span class="separator">•</span>
<span>EXCLUSIVE OFFERS</span>
<span class="separator">•</span>
<span>LATEST UPDATES</span>
<span class="separator">•</span>
<span>SIGN UP NOW</span>
<span class="separator">•</span>
</div>
</div>
</div>
CSS Styling
We use CSS variables for easy configuration. The mask-image property creates the soft fade effect on the left and right edges. The animation simply translates the track.
For the Reverse Direction (Right-to-Left), simply change --direction to reverse or create a modifier class.
:root {
--marquee-speed: 20s;
--marquee-gap: 2rem;
--marquee-text-color: #ffffff;
--marquee-bg: #111;
--fade-width: 100px;
}
body {
background-color: #050505;
color: #fff;
font-family: 'Inter', sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
margin: 0;
}
.marquee-container {
position: relative;
width: 100%;
max-width: 800px; /* Adjust as needed */
background: var(--marquee-bg);
overflow: hidden;
padding: 1rem 0;
/* Gradient Fade Edges */
mask-image: linear-gradient(
to right,
transparent 0%,
black var(--fade-width),
black calc(100% - var(--fade-width)),
transparent 100%
);
-webkit-mask-image: linear-gradient(
to right,
transparent 0%,
black var(--fade-width),
black calc(100% - var(--fade-width)),
transparent 100%
);
}
.marquee-track {
display: flex;
width: max-content;
gap: var(--marquee-gap);
animation: scroll var(--marquee-speed) linear infinite;
}
/* Pause on Hover */
.marquee-container:hover .marquee-track {
animation-play-state: paused;
}
.marquee-content {
display: flex;
align-items: center;
gap: var(--marquee-gap);
font-size: 2rem;
font-weight: 700;
white-space: nowrap;
color: var(--marquee-text-color);
text-transform: uppercase;
}
.separator {
color: #555; /* Dimmer separator */
font-size: 1.5rem;
}
/* Keyframes */
@keyframes scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(-50% - (var(--marquee-gap) / 2)));
}
}
/* Modifiers */
.marquee-track.reverse {
animation-direction: reverse;
}
.marquee-track.fast {
--marquee-speed: 10s;
}
.marquee-track.slow {
--marquee-speed: 40s;
}
JavaScript (Optional Auto-Duplication)
If you don’t want to manually duplicate the HTML content, you can use this simple script to automatically clone the content for the seamless loop.
document.addEventListener('DOMContentLoaded', () => {
const track = document.querySelector('.marquee-track');
const content = track.querySelector('.marquee-content');
// Clone the content
const clone = content.cloneNode(true);
clone.setAttribute('aria-hidden', 'true'); // Accessibility
track.appendChild(clone);
});