Marquees are everywhere: logo strips, testimonial tickers, scrolling headlines. Most WordPress solutions rely on heavy slider plugins or CSS-only animations that pause awkwardly on hover and can’t handle dynamic content widths. Here’s how to build a smooth, infinite marquee for WordPress using GSAP.
What you’ll build
A continuously scrolling text strip that loops seamlessly with no visible reset or jump. Works with text, logos, or any inline content. This basic version scrolls in one direction at a fixed speed, pauses on hover, and respects reduced motion preferences.
Why GSAP instead of CSS?
You can build a basic CSS marquee with @keyframes and translateX. It works for simple cases, but hits limits quickly:
- No pause on hover without a janky
animation-play-statetoggle that resets position - Fixed width means you need to hardcode the translateX value or accept misaligned loops
- No speed control at runtime
- Jumpy resets when the animation loops back to the start
GSAP’s ticker gives you frame-by-frame control over position, so you get smooth looping, hover pausing, and dynamic content widths without fighting CSS limitations. For a deeper comparison, see GSAP vs CSS Animations.
Step 1: Add GSAP
GSAP loads via CDN, so there’s nothing to install. Use a plugin like “WPCode” or “Insert Headers and Footers” to add this script to your site’s footer:
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
No additional plugins needed for this effect. Just the core GSAP library.
Step 2: Add the HTML
Add this markup wherever you want the marquee. In Gutenberg, use a Custom HTML block. In Elementor or Bricks, use an HTML widget.
<div class="marquee">
<div class="marquee-track">
<span class="marquee-item">Your Text Here</span>
<span class="marquee-sep">•</span>
<span class="marquee-item">Another Item</span>
<span class="marquee-sep">•</span>
<span class="marquee-item">Third Item</span>
<span class="marquee-sep">•</span>
</div>
</div>
The separator spans are optional. You can use any content: text, images, SVG logos, or a mix.
Logo strip variant
For a client logo strip, swap the text spans for images:
<div class="marquee">
<div class="marquee-track">
<img class="marquee-logo" src="logo-1.svg" alt="Client name" />
<img class="marquee-logo" src="logo-2.svg" alt="Client name" />
<img class="marquee-logo" src="logo-3.svg" alt="Client name" />
<img class="marquee-logo" src="logo-4.svg" alt="Client name" />
</div>
</div>
Step 3: Add the CSS
Add this via your theme’s custom CSS field, the WordPress Customizer, or WPCode as a CSS snippet:
.marquee {
overflow: hidden;
width: 100%;
padding: 1rem 0;
}
.marquee-track {
display: flex;
gap: 2rem;
width: max-content;
will-change: transform;
}
.marquee-item {
font-size: clamp(2rem, 5vw, 4rem);
font-weight: 700;
text-transform: uppercase;
white-space: nowrap;
flex-shrink: 0;
}
.marquee-sep {
font-size: clamp(2rem, 5vw, 4rem);
opacity: 0.4;
flex-shrink: 0;
}
.marquee-logo {
height: 40px;
width: auto;
flex-shrink: 0;
filter: grayscale(1) brightness(0.8);
transition: filter 0.3s;
}
.marquee-logo:hover {
filter: grayscale(0) brightness(1);
}
A few notes: overflow: hidden on the container clips the content that scrolls off-screen. width: max-content on the track lets it expand to fit all items instead of wrapping. will-change: transform hints to the browser that this element will be animated, enabling GPU compositing.
Step 4: Add the JavaScript
Add this script after the GSAP CDN tag:
document.addEventListener('DOMContentLoaded', function() {
// Respect reduced motion preferences
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
document.querySelectorAll('.marquee').forEach(function(marquee) {
var track = marquee.querySelector('.marquee-track');
if (!track) return;
// Clone content for seamless loop
track.innerHTML += track.innerHTML;
var totalWidth = track.scrollWidth / 2;
var speed = 80; // pixels per second — adjust to taste
var pos = 0;
var paused = false;
// Pause on hover
marquee.addEventListener('mouseenter', function() { paused = true; });
marquee.addEventListener('mouseleave', function() { paused = false; });
// Animate with GSAP ticker for smooth frame-by-frame control
gsap.ticker.add(function() {
if (paused) return;
pos -= speed / 60;
if (pos <= -totalWidth) pos += totalWidth;
gsap.set(track, { x: pos });
});
});
});
Here’s what each part does:
track.innerHTML += track.innerHTMLduplicates all the content inside the track. This creates a seamless loop: when the first set scrolls out of view, the second set is already in position.totalWidth = track.scrollWidth / 2measures the width of one set of content. Whenposreaches this value, we reset it to zero. Because both halves are identical, the reset is invisible.gsap.ticker.add()hooks into GSAP’s internal requestAnimationFrame loop. Instead of creating tweens, we manually update position on every frame. This avoids the overhead of creating and garbage-collecting tween objects 60 times per second.gsap.set()applies the transform without any animation duration, keeping the update instant and letting the ticker handle smoothness.
The speed variable controls how fast the marquee scrolls in pixels per second. 80 is a good starting point. Use 40-60 for a relaxed feel, or 100-150 for something more energetic.
Step 5: Add it to WordPress
You’ve got three pieces: the GSAP CDN tag (Step 1), the CSS (Step 3), and the HTML + JavaScript (Steps 2 and 4). Here’s how to add them.
Option A: WPCode plugin (recommended)
Snippet 1: CSS (site-wide)
Go to Code Snippets > Add Snippet > “Add Your Custom Code”. Set code type to “CSS Snippet”, paste the CSS from Step 3, toggle on, and save.
Snippet 2: Footer scripts (site-wide)
Create another snippet, set code type to “HTML Snippet”, and paste:
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
document.querySelectorAll('.marquee').forEach(function(marquee) {
var track = marquee.querySelector('.marquee-track');
if (!track) return;
track.innerHTML += track.innerHTML;
var totalWidth = track.scrollWidth / 2;
var speed = 80;
var pos = 0;
var paused = false;
marquee.addEventListener('mouseenter', function() { paused = true; });
marquee.addEventListener('mouseleave', function() { paused = false; });
gsap.ticker.add(function() {
if (paused) return;
pos -= speed / 60;
if (pos <= -totalWidth) pos += totalWidth;
gsap.set(track, { x: pos });
});
});
});
</script>
Set insertion location to “Site Wide Footer”. Toggle on and save.
Snippet 3: Marquee HTML (per page)
Add the marquee HTML from Step 2 wherever you need it using Gutenberg’s Custom HTML block, or set it as a site-wide HTML snippet if you want it on every page (e.g., a logo strip above the footer).
Option B: Child theme
Add the CSS to your child theme’s style.css. Then enqueue GSAP and the marquee script via functions.php:
add_action('wp_enqueue_scripts', function() {
wp_enqueue_script('gsap', 'https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js', [], '3.14.2', true);
wp_enqueue_script('marquee', get_stylesheet_directory_uri() . '/js/marquee.js', ['gsap'], '1.0', true);
});
Save the JavaScript from Step 4 as js/marquee.js in your child theme folder.
Common issues
Content not wide enough to loop. If your marquee items don’t fill at least the viewport width, you’ll see a gap before the loop starts. Either add more items or duplicate the content a second time (track.innerHTML += track.innerHTML again for 4x copies).
Caching plugins. Clear your cache (WP Rocket, W3 Total Cache, etc.) after adding or changing the scripts. Cached pages will serve the old version without your marquee code.
Layout shift on load. The marquee HTML renders before JavaScript clones the content, which can cause a brief flash of the un-duplicated content. If this bothers you, add visibility: hidden to .marquee-track in CSS and add gsap.set(track, { visibility: 'visible' }) right after the cloning line in JavaScript.
That’s it
The marquee scrolls continuously, pauses on hover, and loops with no visible jump. Add multiple .marquee containers on the same page and each one runs independently. Users who prefer reduced motion see static content.
To customize: adjust speed for faster or slower scrolling. Change the font size, color, and spacing in the CSS. Swap text for images by using <img> tags inside the track.
Want more control?
The full Infinite Marquee effect includes features this basic version doesn’t have:
- Mouse-position speed control - move your cursor left to rewind, center to pause, right to fast-forward, with 60fps interpolated transitions via
gsap.quickTo - Multiple hover indicator styles - pill labels, icons, glowing dots, or progress bars that show the current speed and direction
- Viewport-aware ticker - automatically pauses the animation when the marquee is off-screen, saving resources on long pages
- Multiple rows - stack marquee rows with independent speeds and directions for layered visual depth
- Configurable via data attributes -
data-direction,data-speed, anddata-hover-styleso you can control each marquee from the HTML alone - Lenis smooth scroll integration - works seamlessly alongside Lenis for pages that use smooth scrolling
- Touch support - tap to pause/play on mobile devices
- Clean destroy method - proper cleanup for single-page apps and dynamic page transitions
View the full effect with all options ->
Related articles
- Adding a Text Scramble Effect to WordPress - Another step-by-step WordPress tutorial using GSAP and ScrollTrigger.
- Custom Cursor Effects for WordPress - Build a custom cursor follower for WordPress with magnetic hover and trail effects.
- GSAP vs CSS Animations: A Practical Guide - When CSS transitions are enough and when you need JavaScript animation.
- Animate on Scroll: 3 Approaches Compared - Compare CSS scroll animations, Intersection Observer, and GSAP ScrollTrigger.