If you want your website to feel premium, smooth, and “wow” at first glance — a GSAP marquee slider is a game-changer. And the best part? You can build it using Elementor Pro without making things complicated.
First, understand why GSAP? GSAP (GreenSock Animation Platform) gives ultra-smooth, professional-level animation. Unlike basic scrolling text, a GSAP marquee feels modern, continuous, and luxurious — perfect for showcasing services, client logos, testimonials, or offers.
Here’s the simple process:
Start by creating a section in Elementor Pro. Add a container and place your content inside it — this could be text, icons, or images. Duplicate the content so the marquee loops smoothly without gaps.
Next, add a CSS class to the container (for example: marquee-slider). Then insert custom CSS and GSAP script in the page settings or using an HTML widget. The script will animate the container horizontally in an infinite loop.
The key is:
~ Use overflow: hidden;
~ Keep content in one row (flex layout)
~ Duplicate items for seamless looping
~ Control speed using GSAP duration
Use overflow: hidden;
Keep content in one row (flex layout)
Duplicate items for seamless looping
Control speed using GSAP duration
Once done, your website instantly looks more dynamic and high-end.
If you’re building WordPress sites for clients, adding subtle animations like this increases perceived value — and that means higher conversions and premium positioning.
<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>