How to make a GSAP Marquee Slider?

How to make a GSAP Marquee Slider?

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>
https://youtu.be/t29q7DTaDzo?si=Sxf2jgJpMDObpAx5

Leave a Comment

Your email address will not be published. Required fields are marked *