Creating a Smooth Marquee Slider with GSAP in Elementor Pro

elementor marquee effect
Picture of Rino de Boer

Rino de Boer

Web Designer & Content Creator

The marquee effect is a slider animation that can display anything, like text, icons, logo’s, SVG’s or any other image. It’s a really popular effect and can be used in a horizontal or diagonal way.

I created some demos with different implementations of it

Cool right? Let’s see how to make it work.

What techniques are being used

We’re using GSAP, a popular JavaScript library for smooth animations.

We are applying this on Elementor Pro, but since we are pasting a code into a HTML widget the chance is very high that this also works inside of other pagebuilders. But I didn’t test that.

I know some of you may not want to work with code. But trust me, it looks more scary than it is.

Setting up Elementor

  1. Add a Container Widget
    • Under Layout tab, set Width to 100%
    • Under Layout tab, set Gaps to 0
    • Under Layout tab > Additional Options, set Overflow to Hidden
    • Set Left & Right Padding to 0
container overflow hidden
  1. Add HTML Widget inside the Container
    • Copy and paste the code below into the HTML Widget

Now paste the following code into the HTML widget.

<script>
(function() {
// ============================================
// MARQUEE CONFIGURATION - CUSTOMIZE HERE
// ============================================

// UNIQUE ID (change for each marquee: "main", "hero", "footer", etc.)
const marqueeId = "main";

// MARQUEE CONTENT - Array format supports text, symbols, images, and HTML
const marqueeContent = ["WordPress Tutorials"];

// Examples:
// Multiple Texts, Icons: ["CREATIVE", "★", "DESIGN", "✦", "BUILD"]
// Images: ['<img src="/wp-content/uploads/2025/08/hello-1.svg" style="height: 1em;">']

// REPEAT COUNT
const repeatCount = 7; // How many times to repeat the pattern

// VISUAL & RESPONSIVE SETTINGS
const marqueeSettings = {
// DESKTOP SETTINGS (default)
fontSize: 32, // Text size in pixels
fontWeight: "400", // Font weight: "normal", "bold", "600", "700", etc.
fontFamily: "inherit", // Font: "Arial", "Helvetica", "inherit" (uses site font)
textColor: "#ffffff", // Text color: "#000000", "black", "inherit" (uses site color)
letterSpacing: "0px", // Letter spacing: "0px", "2px", "5px", etc.
textTransform: "none", // Text transform: "none", "uppercase", "lowercase", "capitalize"
lineHeight: "1.2", // Line height: "1", "1.2", "1.5", "2", etc.
padding: 48, // Space between repeating blocks in pixels

// BEHAVIOR SETTINGS
speed: 0.5, // Animation speed (higher = faster, 0.1 = very slow, 5 = very fast)
reverse: false, // false = left to right, true = right to left
pauseOnHover: true, // true = pause when mouse over, false = keep moving

// RESPONSIVE BEHAVIOR
responsiveEnabled: true, // Set to false to disable responsive behavior
breakpoints: {
tablet: 1024, // Max width for tablet
mobile: 768 // Max width for mobile
},

// TABLET SETTINGS (768-1024px)
// Set any value to null to use desktop value
tabletFontSize: 24,
tabletPadding: 32,
tabletSpeed: 1,

// MOBILE SETTINGS (<768px)
// Set any value to null to use desktop value
mobileFontSize: 18,
mobilePadding: 20,
mobileSpeed: 0.8
};

// ============================================
// END CONFIGURATION - DON'T EDIT BELOW
// ============================================

// Generate class names based on marqueeId
const wrapperClass = `mq-wrapper-${marqueeId}`;
const innerClass = `mq-inner-${marqueeId}`;
const partClass = `mq-part-${marqueeId}`;

// Get responsive values based on screen width
function getResponsiveValue(settingName) {
if (!marqueeSettings.responsiveEnabled) {
return marqueeSettings[settingName];
}

const width = window.innerWidth;
const breakpoints = marqueeSettings.breakpoints;

// Mobile
if (width <= breakpoints.mobile) {
const mobileKey = 'mobile' + settingName.charAt(0).toUpperCase() + settingName.slice(1);
return marqueeSettings[mobileKey] !== null && marqueeSettings[mobileKey] !== undefined
? marqueeSettings[mobileKey]
: marqueeSettings[settingName];
}

// Tablet
if (width <= breakpoints.tablet) {
const tabletKey = 'tablet' + settingName.charAt(0).toUpperCase() + settingName.slice(1);
return marqueeSettings[tabletKey] !== null && marqueeSettings[tabletKey] !== undefined
? marqueeSettings[tabletKey]
: marqueeSettings[settingName];
}

// Desktop
return marqueeSettings[settingName];
}

// Generate HTML content
let htmlContent = '';
for (let i = 0; i < repeatCount; i++) {
marqueeContent.forEach(item => {
htmlContent += `<div class="${partClass}">${item}</div>`;
});
}

// Generate HTML dynamically
document.currentScript.insertAdjacentHTML('afterend', `
<div class="${wrapperClass}">
<div class="${innerClass}">
${htmlContent}
</div>
</div>
`);

// Generate CSS dynamically
const style = document.createElement('style');
style.textContent = `
.${wrapperClass} {
width: 100%;
overflow: hidden;
position: relative;
}

.${innerClass} {
display: flex;
align-items: center;
width: fit-content;
position: relative;
}

.${partClass} {
flex-shrink: 0;
white-space: nowrap;
display: inline-flex;
align-items: center;
}

.${partClass} img {
display: inline-block;
vertical-align: middle;
}
`;
document.head.appendChild(style);

// Apply styles based on current viewport
function applyResponsiveStyles() {
const fontSize = getResponsiveValue('fontSize');
const padding = getResponsiveValue('padding');

// FIX: Only apply non-responsive styles once to avoid overriding responsive values
const parts = document.querySelectorAll('.' + partClass);
parts.forEach(el => {
el.style.fontSize = fontSize + 'px';
el.style.padding = `0 ${padding}px`;

// Only apply these if not already set (to avoid overriding on resize)
if (!el.style.fontWeight) {
el.style.fontWeight = marqueeSettings.fontWeight;
el.style.fontFamily = marqueeSettings.fontFamily;
el.style.color = marqueeSettings.textColor;
el.style.letterSpacing = marqueeSettings.letterSpacing;
el.style.textTransform = marqueeSettings.textTransform;
el.style.lineHeight = marqueeSettings.lineHeight;
}
});
}

// Initialize marquee animation
let marqueeAnimation = null;
let resizeTimer = null;
let currentBreakpoint = null;
let eventListenersAdded = false; // FIX: Track if event listeners have been added

function getBreakpoint() {
const width = window.innerWidth;
if (width <= marqueeSettings.breakpoints.mobile) return 'mobile';
if (width <= marqueeSettings.breakpoints.tablet) return 'tablet';
return 'desktop';
}

function initializeMarquee() {
// Kill existing animation if it exists
if (marqueeAnimation) {
marqueeAnimation.kill();
marqueeAnimation = null; // FIX: Clear the reference
}

// Apply responsive styles
applyResponsiveStyles();

// Get current speed
const currentSpeed = getResponsiveValue('speed');

// GSAP horizontal loop function - FIXED VERSION
function horizontalLoop(items, config) {
items = gsap.utils.toArray(items);

// FIX: Early return if no items found
if (!items || items.length === 0) {
console.warn('No marquee items found for selector');
return null;
}

config = config || {};
let tl = gsap.timeline({
repeat: config.repeat,
paused: config.paused,
defaults: {ease: "none"}
// Removed onReverseComplete to prevent direction switching issues
}),
length = items.length,
startX = items[0].offsetLeft,
times = [],
widths = [],
xPercents = [],
pixelsPerSecond = (config.speed || 1) * 100,
snap = config.snap === false ? v => v : gsap.utils.snap(config.snap || 1),
totalWidth, curX, distanceToStart, distanceToLoop, item, i;

gsap.set(items, {
xPercent: (i, el) => {
let w = widths[i] = parseFloat(gsap.getProperty(el, "width", "px"));
xPercents[i] = snap(parseFloat(gsap.getProperty(el, "x", "px")) / w * 100 + gsap.getProperty(el, "xPercent"));
return xPercents[i];
}
});

gsap.set(items, {x: 0});

totalWidth = items[length-1].offsetLeft + xPercents[length-1] / 100 * widths[length-1] - startX + items[length-1].offsetWidth * gsap.getProperty(items[length-1], "scaleX") + (parseFloat(config.paddingRight) || 0);

for (i = 0; i < length; i++) {
item = items[i];
curX = xPercents[i] / 100 * widths[i];
distanceToStart = item.offsetLeft + curX - startX;
distanceToLoop = distanceToStart + widths[i] * gsap.getProperty(item, "scaleX");

tl.to(item, {
xPercent: snap((curX - distanceToLoop) / widths[i] * 100),
duration: distanceToLoop / pixelsPerSecond
}, 0)
.fromTo(item, {
xPercent: snap((curX - distanceToLoop + totalWidth) / widths[i] * 100)
}, {
xPercent: xPercents[i],
duration: (curX - distanceToLoop + totalWidth - curX) / pixelsPerSecond,
immediateRender: false
}, distanceToLoop / pixelsPerSecond)
.add("label" + i, distanceToStart / pixelsPerSecond);

times[i] = distanceToStart / pixelsPerSecond;
}

tl.progress(1, true).progress(0, true);

// FIXED: Inverted the logic - now false = left to right, true = right to left
if (!config.reversed) {
// For left to right movement, reverse the timeline
tl.reverse(0);
}

return tl;
}

// Create marquee animation with fixed reverse logic
marqueeAnimation = horizontalLoop("." + partClass, {
repeat: -1,
speed: currentSpeed,
reversed: marqueeSettings.reverse, // Now properly mapped
paused: false
});

// FIX: Check if animation was created successfully
if (!marqueeAnimation) {
return;
}

// Add pause on hover if enabled - FIXED VERSION
if (marqueeSettings.pauseOnHover) {
const wrapper = document.querySelector('.' + wrapperClass);
if (wrapper) {
// Remove old handlers if they exist
if (wrapper._mouseEnterHandler) {
wrapper.removeEventListener('mouseenter', wrapper._mouseEnterHandler);
wrapper.removeEventListener('mouseleave', wrapper._mouseLeaveHandler);
}

// Create new handlers that preserve timeline direction
wrapper._mouseEnterHandler = () => {
if (marqueeAnimation) {
marqueeAnimation.pause();
}
};

wrapper._mouseLeaveHandler = () => {
if (marqueeAnimation) {
// Resume without changing direction
marqueeAnimation.resume();
}
};

wrapper.addEventListener('mouseenter', wrapper._mouseEnterHandler);
wrapper.addEventListener('mouseleave', wrapper._mouseLeaveHandler);
}
}
}

// Handle resize events
function handleResize() {
if (!marqueeSettings.responsiveEnabled) return;

clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
const newBreakpoint = getBreakpoint();
// Only reinitialize if breakpoint changed
if (newBreakpoint !== currentBreakpoint) {
currentBreakpoint = newBreakpoint;
initializeMarquee();
}
}, 250); // Debounce resize events
}

// FIX: Ensure GSAP is loaded before initialization
function waitForGSAP(callback) {
if (typeof gsap !== 'undefined') {
callback();
} else {
// Retry after a short delay
setTimeout(() => waitForGSAP(callback), 50);
}
}

// Initialize on page load
window.addEventListener('load', function() {
waitForGSAP(() => {
currentBreakpoint = getBreakpoint();
initializeMarquee();

// Add resize listener if responsive is enabled (only once)
if (marqueeSettings.responsiveEnabled && !eventListenersAdded) {
window.addEventListener('resize', handleResize);
eventListenersAdded = true;
}
});
});

// Also try to initialize earlier for Elementor editor
if (document.readyState === 'complete' || document.readyState === 'interactive') {
setTimeout(() => {
waitForGSAP(() => {
// FIX: Check if already initialized to avoid duplicate initialization
if (!marqueeAnimation) {
currentBreakpoint = getBreakpoint();
initializeMarquee();

if (marqueeSettings.responsiveEnabled && !eventListenersAdded) {
window.addEventListener('resize', handleResize);
eventListenersAdded = true;
}
}
});
}, 100);
}

})();
</script>

<!-- GSAP Library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>

Publish the changes and have a look at the page on the front end. You’ll already see a smooth marquee effect. It works right out of the box.

Now you can just customize it to your liking.

Customizing

As you saw in the demos earlier, there are 3 different marquee effects that are possible with this code.

All 3 versions can be achieved from the same code.

Let’s understand the code so we can make the changes.

Understand Commenting

The parts of the code that look purple (or green in Elementor light mode) are ignored by the web. These notes/comments are just for us to make sure we don’t lose ourselves in the code.

The ones that start with // are single-line comment. Whatever goes after it on the same line is treated as a comment and therefor ignored by the browser.

There are also multi-line comments. Anything between /* code here */ is treated as a comment. Also ignored.

Marquee Code in Elementor HTML Widget
When you drop the code it should look like this

Different Sections of the Code

There are 3 main sections to the code:

1 – Unique ID

If you’re planning to use more than one marquee on the same page, you have to make sure each one has a different ID.

In our code, the first option is to change the ID:

// UNIQUE ID (change for each marquee: "main", "hero", "footer", etc.)
const marqueeId = "main"; 

“main” is the ID of this marquee. If you want to create another marquee on the same page, you have to add a new Elementor HTML widget first. Copy and paste the same code into it.

Then you must change the ID name. For example, “marquee-home”.

If you have just one marquee on the page, you don’t have to do anything here.

2 – Marquee Content

Then comes the content section. This is where you add the content you want to display in the marquee.

// MARQUEE CONTENT - Array format supports text, symbols, images, and HTML
const marqueeContent = ["Living With Pixels"];

// Examples:
// Multiple Texts, Icons: ["CREATIVE", "★", "DESIGN", "✦", "BUILD"]
// Images: ['<img src="/wp-content/uploads/2025/08/hello-1.svg" style="height: 1em;">']

See the text “Living With Pixels”? You have to replace your content there within “”.

Under examples, you can see how to use different elements together in the marquee. You’ll see an example soon.

3 – Marquee Settings

The next section of the code is for settings, like styling and speed:

// REPEAT COUNT
const repeatCount = 7; // How many times to repeat the pattern

// VISUAL & RESPONSIVE SETTINGS
const marqueeSettings = {
    // DESKTOP SETTINGS (default)
    fontSize: 32,               // Text size in pixels
    fontWeight: "400",          // Font weight: "normal", "bold", "600", "700", etc.
    fontFamily: "inherit",      // Font: "Arial", "Helvetica", "inherit" (uses site font)
    textColor: "#5900FF",       // Text color: "#000000", "black", "inherit" (uses site color)
    letterSpacing: "0px",       // Letter spacing: "0px", "2px", "5px", etc.
    textTransform: "none",      // Text transform: "none", "uppercase", "lowercase", "capitalize"
    lineHeight: "1.2",          // Line height: "1", "1.2", "1.5", "2", etc.
    padding: 48,                // Space between repeating blocks in pixels
    
    // BEHAVIOR SETTINGS
    speed: 1,                   // Animation speed (1 = slow, 5 = fast)
    reverse: false,             // true = right to left, false = left to right
    pauseOnHover: true,         // true = pause when mouse over, false = keep moving
    
    // RESPONSIVE BEHAVIOR
    responsiveEnabled: true,    // Set to false to disable responsive behavior
    breakpoints: {
        tablet: 1024,           // Max width for tablet
        mobile: 768             // Max width for mobile
    },
    
    // TABLET SETTINGS (768-1024px)
    // Set any value to null to use desktop value
    tabletFontSize: 24,
    tabletPadding: 32,
    tabletSpeed: 1,
    
    // MOBILE SETTINGS (<768px)
    // Set any value to null to use desktop value
    mobileFontSize: 18,
    mobilePadding: 20,
    mobileSpeed: 0.8
};

 The first option “REPEAT COUNT”.

 This sets how many times the text repeats to create a smooth scrolling effect. The text needs to repeat enough times to fill (and exceed) the screen width. Otherwise, you’ll see gaps in the animation.

Example:

  • Short text like “WordPress” needs more repeats (maybe 7) to fill the screen
  • Longer text like “Best WordPress Web Agency In Florida” needs fewer repeats (maybe 3) since it’s already wider

You have to adjust this number based on text length. Shorter text needs higher values, longer text needs lower values.

Next, under “// VISUAL & RESPONSIVE SETTINGS”, there are a number of different options to customize this marquee.

First, there are options related to styling like font size, font color, font family, etc. You can see each one has a comment describing what it is.

Under “// BEHAVIOR SETTINGS”, it also has controls related to how the marquee behaves. Like speed, direction, pause on hover or not.

Under “// RESPONSIVE BEHAVIOR”, you can make sure your marquee looks good on tablet and mobile.

If you don’t want different font sizes or gaps in between on mobile or tablet, change responsiveEnabled: true to false.

There are comments describing each control so you don’t get lost.

That’s it. Using those controls, you can easily customize and manage the marquee to your taste.

Single Item Marquee

Let’s start creating a marquee with a single text. That means we just want to loop one single text infinitely.

It could be a single word, sentence, icon, or SVG image.

If you prefer videos, have a look at below where I am making changes to this code.

  1. Add a container > Full Width, Gap: 0, Overflow: Hidden
  2. Add Elementor HTML Widget inside it and copy and paste the code into it

Unfortunately, you won’t see a live preview in the Elementor Editor. Save and check the actual page to see the results. Just open the result page in a new tab, so you can constantly refresh, that’s faster than constantly having to load the Elementor editor.

As mentioned earlier you have to edit what’s between these 2 sign [] in marqueeContent. By default it has “Living with Pixels”.

Screenshot of Maqeuee code Edit Content area

Let’s change it “WordPress Tutorials” so our content will be that. Make sure to include your text within “” as seen in the. screenshot.

Screenshot of Maqeuee code Edit Content area with up dated content

If you preview now, we have a smooth, working marquee on our page.

Next you can style the text using options under // VISUAL & RESPONSIVE SETTINGS. As you can see it has options like font size, font weight, text color, etc.

I have added some examples as well so you are very clear about each settings. I will just update the text color to white(ffffff).

Marquee Code Settings

Adding a Background Color

Next, I want to style the background color as well. To style things outside the actual content (text in this example), we have to use the Elementor Builder.

Since we’re using HTML widget, go to the Advanced tab. From there, you can style the container with background color, padding, borders, and more.

In this example, I’m just adding a background color #7A64FF.

Adding Background Color to Elementor HTML Widget

Next, I’m going to set the speed to 0.5 to slow it down a bit under “// BEHAVIOR SETTINGS”.

Marquee Speed Setting in Code

Preview it and see, you’ll have a nice marquee.

single text marquee demo

Under // RESPONSIVE BEHAVIOR you can change font size on tablet and mobile to your liking to make sure it’s responsive.

In practice, you’ll just have to keep this container in between your other containers.

Marquee with Multiple Items

Now let’s start creating a marquee with multiple different items. That means the marquee can have different items.

It could be a mix of text, icons, or SVG images. Like this example. (Below demo is not smooth as it’s a GIF).

a marquee demo with multiple items

Similar to before we have to add our content between [] in marqueeContent.

Under // Example you can see how to add text, icons, images and html items.

You have to add your content within “” inside []. For images use (single quotes) instead of “” (double quotes) as we need “” these to wrap logo link and style properties.

// MARQUEE CONTENT - Array format supports text, symbols, images, and HTML
const marqueeContent = ["Living With Pixels"];

// Examples:
// Multiple Texts, Icons: ["CREATIVE", "★", "DESIGN", "✦", "BUILD"]
// Images: ['<img src="/wp-content/uploads/2025/08/hello-1.svg" style="height: 1em;">']

For example wee need WordPress, ★, Elementor, {image} for content of our marquee. As you can see below first we have “WordPress” then a comma (,) then the next element. In this case an unicode character icon still within “★”.

For the image just copy the example code given in // Example and paste it just like below. You can upload the logo image to WordPress media library and just copy the link and paste as the src.

I hope you get the idea. For our example I am changing the code as below.

// MARQUEE CONTENT - Array format supports text, symbols, images, and HTML
const marqueeContent = ["WordPress", "★", "Elementor", '<img src="/wp-content/uploads/img.svg" style="height: 1em;">'];
how to edit marquee code content2

Next I want to add a background color. So go to the Advanced tab of HTML widget > Background to add a background color.

add background to html widget

I hope you’re clear on how to create a marquee on your Elementor website now.

Combined Marquee – Bonus

As a bonus, I wanted to show you an example of a combined marquee. Basically, we have 2 marquees that create a nice cool effect.

For the first marquee I want “Living With Pixels” and ✦.

As you can see below I have updated the content accordingly. I have also customized some styling options. Font size, font weight, text color and text transform.

Marquee Code

As you can see above I have set the font size to 120px. Since this font size is very big, it won’t be suitable for tablet and mobile.

So let’s changed the font size accordingly on Tablet and Mobile using the given controls in the code.

Marquee Code Responsive Settings

Next as we did to previous marquees let’s add a background color.

As you know background colors are added to the HTML widget. Under HTML widget > Advanced > Background.

add background to html widget

So far we created a Marquee similar to previous examples like this:

marquee demo 75

If you check the demo I showed you earlier, the Marquee should be rotated. So let’s do that.

The easiest way to add rotation is to got to HTML Widget > Advanced > Transform. But once rotated it shows a gap on left and right of the marquee.

The solution is to add our HTML Widget inside another Inner Container. So add a new Container inside our main Container and move the HTML Widget there.

Make sure to set the Inner Container to Full Width and remove padding, margin and gaps.

ss2025 10 14 at 14.39.29

Next to rotate our marquee, select Inner Container and go to Advanced > Transform and set like 4 deg. You could adjust it to your liking.

ss2025 10 14 at 14.43.43

If you preview the page now, you’ll see there’s a gap from left and right.

ss2025 10 14 at 14.45.48

The solution is very simple. Just go to Inner Container > Advanced > Scale and add some like 1.05. It’ll fix the issue and you have a nice rotated marquee.

ss2025 10 14 at 14.53.00

That’s the first marquee done.

Second Marquee

Next, create another marquee. I am going to duplicate the the first marquee with the inner container.

ss2025 10 14 at 14.53.56

Since this is the 2nd Marquee on the same page, we must use a different marqueeID. As you can see below I have changed the marqueeID from main to secondary.

I have changed the content too as “Helping designers turn their skills into sustainable businesses.”

ss2025 10 14 at 14.57.37

Make sure to change styling as well. I have changes font size and text color of this second marquee using the code. And changes the background color of HTML Widget to bright like like color.

This is how it looks like now:

ss2025 10 14 at 15.00.36

As you can see what’s left is to rotate the second marquee the other way.

Remember we rotated the first one 4deg? Let’s rotate this 5deg. So go to second marquee’s Inner Container > Advanced > Transform set the value to 5.

ss2025 10 14 at 15.03.20

Now you’ll see a nice marquee with overlapping the two.

combined marquee final results

Outro

I’m very happy with this cool marquee effect and about this method of achieving it.

I hope you can play with it and implement it on your sites.

Thanks for reading!