# Adding an Infinite Marquee to WordPress

> Build a smooth infinite marquee for WordPress using GSAP. Scrolling text, logo strips, and ticker bars with no plugin required.

Published 2026-04-04 by Jake. Canonical: https://gsapvault.com/blog/infinite-marquee-wordpress

---
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. If you have not set GSAP up on the site yet, [GSAP in WordPress](/gsap-wordpress) covers the enqueue, the page builder gotchas and the caching traps first.

## 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.

<div id="marquee-demo" style="position: relative; overflow: hidden; min-width: 0; border: 1px solid rgba(200, 255, 0, 0.2); border-radius: 12px; background: rgba(0, 0, 0, 0.3); padding: 1.5rem 0; margin: 1.5rem 0 2rem; user-select: none;">
  <div id="marquee-demo-track" style="display: flex; gap: 2rem; width: max-content; will-change: transform;">
    <span style="font-family: 'Syne', sans-serif; font-size: clamp(1.5rem, 4vw, 2.5rem); font-weight: 700; color: var(--text, #e5e5e5); text-transform: uppercase;">Design</span>
    <span style="color: var(--lime, #c8ff00); font-size: clamp(1.5rem, 4vw, 2.5rem);">&bull;</span>
    <span style="font-family: 'Syne', sans-serif; font-size: clamp(1.5rem, 4vw, 2.5rem); font-weight: 700; color: var(--text, #e5e5e5); text-transform: uppercase;">Develop</span>
    <span style="color: var(--lime, #c8ff00); font-size: clamp(1.5rem, 4vw, 2.5rem);">&bull;</span>
    <span style="font-family: 'Syne', sans-serif; font-size: clamp(1.5rem, 4vw, 2.5rem); font-weight: 700; color: var(--text, #e5e5e5); text-transform: uppercase;">Animate</span>
    <span style="color: var(--lime, #c8ff00); font-size: clamp(1.5rem, 4vw, 2.5rem);">&bull;</span>
    <span style="font-family: 'Syne', sans-serif; font-size: clamp(1.5rem, 4vw, 2.5rem); font-weight: 700; color: var(--text, #e5e5e5); text-transform: uppercase;">Ship</span>
    <span style="color: var(--lime, #c8ff00); font-size: clamp(1.5rem, 4vw, 2.5rem);">&bull;</span>
    <span style="font-family: 'Syne', sans-serif; font-size: clamp(1.5rem, 4vw, 2.5rem); font-weight: 700; color: var(--text, #e5e5e5); text-transform: uppercase;">Design</span>
    <span style="color: var(--lime, #c8ff00); font-size: clamp(1.5rem, 4vw, 2.5rem);">&bull;</span>
    <span style="font-family: 'Syne', sans-serif; font-size: clamp(1.5rem, 4vw, 2.5rem); font-weight: 700; color: var(--text, #e5e5e5); text-transform: uppercase;">Develop</span>
    <span style="color: var(--lime, #c8ff00); font-size: clamp(1.5rem, 4vw, 2.5rem);">&bull;</span>
    <span style="font-family: 'Syne', sans-serif; font-size: clamp(1.5rem, 4vw, 2.5rem); font-weight: 700; color: var(--text, #e5e5e5); text-transform: uppercase;">Animate</span>
    <span style="color: var(--lime, #c8ff00); font-size: clamp(1.5rem, 4vw, 2.5rem);">&bull;</span>
    <span style="font-family: 'Syne', sans-serif; font-size: clamp(1.5rem, 4vw, 2.5rem); font-weight: 700; color: var(--text, #e5e5e5); text-transform: uppercase;">Ship</span>
    <span style="color: var(--lime, #c8ff00); font-size: clamp(1.5rem, 4vw, 2.5rem);">&bull;</span>
  </div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;

  var track = document.getElementById('marquee-demo-track');
  if (!track) return;

  // Clone content for seamless loop
  track.innerHTML += track.innerHTML;

  var totalWidth = track.scrollWidth / 2;
  var speed = 60; // pixels per second
  var pos = 0;
  var paused = false;

  track.parentElement.addEventListener('mouseenter', function() { paused = true; });
  track.parentElement.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>

## 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-state` toggle 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](/blog/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:

```html
<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.

```html
<div class="marquee">
  <div class="marquee-track">
    <span class="marquee-item">Your Text Here</span>
    <span class="marquee-sep">&bull;</span>
    <span class="marquee-item">Another Item</span>
    <span class="marquee-sep">&bull;</span>
    <span class="marquee-item">Third Item</span>
    <span class="marquee-sep">&bull;</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:

```html
<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:

```css
.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:

```javascript
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.innerHTML`** duplicates 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 / 2`** measures the width of one set of content. When `pos` reaches 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](https://developer.mozilla.org/en-US/docs/Web/API/Window/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:

```html
<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`:

```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](/effects/infinite-marquee) 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`, and `data-hover-style` so 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 ->](/effects/infinite-marquee)

---

## Related articles

- **[GSAP in WordPress: the complete guide](/gsap-wordpress)** - Where to enqueue GSAP, block editor and page builder realities, and every WordPress tutorial on this site in one place.
- **[Adding a Text Scramble Effect to WordPress](/blog/text-scramble-wordpress)** - Another step-by-step WordPress tutorial using GSAP and ScrollTrigger.
- **[Custom Cursor Effects for WordPress](/blog/custom-cursor-effects-wordpress)** - Build a custom cursor follower for WordPress with magnetic hover and trail effects.
- **[GSAP vs CSS Animations: A Practical Guide](/blog/gsap-vs-css-animations)** - When CSS transitions are enough and when you need JavaScript animation.
- **[Animate on Scroll: 3 Approaches Compared](/blog/how-to-animate-on-scroll)** - Compare CSS scroll animations, Intersection Observer, and GSAP ScrollTrigger.