JavaScript Marquee: A collection of scrolling text snippets

on in JavaScript Carousels
Last modified on

JavaScript Marquee / Scrolling Text
JavaScript Marquee / Scrolling Text

I have started to consider adding scrolling text into my design process. I started by using a CSS-only approach. The principle for the CSS infinite scrolling process is to have a specific number of clones of the content. As for how many, it depends on the parent width.

Table of Contents

CSS Marquee/Scrollable Text #1

Let’s take the example below:

<div class="wrapper">
    <div class="marquee">
        <div>
            <span>This content will scroll.</span>
            <span>This content will also scroll.</span>
        </div>
        <div>
            <span>This content will scroll.</span>
            <span>This content will also scroll.</span>
        </div>
    </div>
</div>

We have a wrapper, which needs to hide any content overflow, and then we have the content, twice. Depending on the length, we could have 3 or 4 copies. Or even 20, if we have one scrolling word.

.wrapper {
    max-width: 100%;
    overflow: hidden;
}
.marquee {
    white-space: nowrap;
    overflow: hidden;
    display: inline-block;
    animation: marquee 10s linear infinite;
}
.marquee div {
    display: inline-block;
}

@keyframes marquee {
    0% {
        transform: translate(0, 0);
    }

    100% {
        transform: translate(-50%, 0);
    }
}

The problem is that, sometimes, the content may get glitchy, based on the page scrolling position, or some other resource loading process that freezes the page. Also, it’s not really flexible, as you need to make sure you have the right amount of cloned items, and you need to update all clones, instead of only one.

See this in action here.

CSS Marquee/Scrollable Text #2

I have another CSS-only marquee that I am currently using on some websites. This one needs to have a specific width set up, but it does not need cloned content. The trick is using the text-shadow property. Basically, we give the text a number of shadows at specific intervals.

<div class="marquee marquee--long alignfull" style="--tw: 80ch; --ad: 12s;">
    <span>lightweight, ad-free, modern code — industry-standard, no-dependency, no tracking</span>
</div>
.marquee {
    background-color: #a855f7;
    border-top: 1px solid #000000;
    color: #ffffff;
    font-family: var(--font-heading);
    font-feature-settings: unset;

    font-size: 18px;
    overflow: hidden;
    padding: 12px 0;
}
.marquee--long { 
    font-size: 14px;
}
.marquee span {   
    display: inline-block;
    white-space: nowrap;
    color: #ffffff;
    width: var(--tw);

    text-shadow: var(--tw) 0 currentColor, 
                 calc(var(--tw) * 2) 0 currentColor, 
                 calc(var(--tw) * 3) 0 currentColor,
                 calc(var(--tw) * 4) 0 currentColor,
                 calc(var(--tw) * 5) 0 currentColor,
                 calc(var(--tw) * 6) 0 currentColor,
                 calc(var(--tw) * 7) 0 currentColor;

    will-change: transform;
    animation: marquee var(--ad) linear infinite;
    animation-play-state: running;
}

@keyframes marquee {
    0% { transform: translateX(0); }
    100% { transform: translateX(-100%); }
}

You can see this one in action on my plugins page.

JavaScript Marquee/Scrollable Text #1

This is where JavaScript comes into play. It usually duplicates the content automatically and calculates everything, such as the length of the content, automatically. There are multiple ways of approaching this, so here are some alternatives.

<div class="wrapper">
    <div class="marquee">Hey there!</div>
</div>

This one stops on hover, and it also clones the content 10 times. Or, you can set it to 50 just to be sure.

const marquees = [...document.querySelectorAll('.marquee')];

marquees.forEach((marquee) => {
    marquee.innerHTML = marquee.innerHTML + '&nbsp;'.repeat(5);
    marquee.i = 0;
    marquee.step = 3;
    marquee.width = marquee.clientWidth + 1;
    marquee.style.position = '';
    marquee.innerHTML = `${marquee.innerHTML}&nbsp;`.repeat(10);

    marquee.addEventListener('mouseenter', () => (marquee.step = 0), false);
    marquee.addEventListener('mouseleave', () => (marquee.step = 3), false);
});

requestAnimationFrame(move);

function move() {
    marquees.forEach((marquee) => {
        marquee.style.marginLeft = `-${marquee.i}px`;
        marquee.i = marquee.i < marquee.width ? marquee.i + marquee.step : 1;
    });

    requestAnimationFrame(move);
}
.wrapper {
    border: 1px solid red;
    width: 100%;
    height: 40px;
    overflow: hidden;
    position: relative;
}
.marquee {
    overflow: hidden;
    white-space: nowrap;
    position: absolute;
}

I like this one. I used it a few times and got no glitches.

See it in action here.

JavaScript Marquee/Scrollable Text #2

This one has a different approach. All marquee clones are absolutely positioned. This makes it even more resilient to glitches, as each element is followed by another one, accurately positioned to the right.

<div class="marquee">
    <span>One</span>
    <span>Two</span>
    <span>Three</span>
    <span>Four</span>
    <span>Five</span>
    <span>Six</span>
    <span>Seven</span>
    <span>Eight</span>
    <span>Nine</span>
    <span>Ten</span>
    <span>Eleven</span>
    <span>Twelve</span>
    <span>Thirteen</span>
    <span>Fourteen</span>
    <span>Fifteen</span>
    <span>Sixteen</span>
    <span>Seventeen</span>
    <span>Eighteen</span>
    <span>Nineteen</span>
    <span>Twenty</span>
</div>

It also artificially adds a non-breaking space between elements.

const off = 10;
let l = off;
const marqueeElements = Array.from(document.querySelectorAll('.marquee span'));
const speed = 2;
const stack = [];
let pause = false;

marqueeElements.forEach(element => {
    const width = element.offsetWidth + off;
    element.style.left = `${l}px`;
    l += width;
    stack.push(element);
});

function moveMarquee() {
    if (!pause) {
        marqueeElements.forEach(element => {
            const currentLeft = parseFloat(getComputedStyle(element).left);
            const newLeft = currentLeft - speed;
            element.style.left = `${newLeft}px`;

            if (newLeft + element.offsetWidth < -130) {
                const firstElement = stack.shift();
                const lastElement = stack[stack.length - 1];
                element.style.left = `${parseFloat(getComputedStyle(lastElement).left) + lastElement.offsetWidth + off}px`;
                stack.push(element);
            }
        });
    }
    requestAnimationFrame(moveMarquee);
}

requestAnimationFrame(moveMarquee);

const marqueeContainer = document.querySelector('.marquee');
marqueeContainer.addEventListener('mouseover', () => {
    pause = true;
});
marqueeContainer.addEventListener('mouseout', () => {
    pause = false;
});

This might be the right option if the marquee elements are being added dynamically.

Don’t forget the CSS:

.marquee span {
    position: absolute;
}

See it in action here.

JavaScript Marquee/Scrollable Text #3

Another good one that can have multiple instances with multiple speeds on the same page is the one below. Note that the borders and the margins have been added for debugging purposes.

<p>Some text here</p>

<div class="marquee-wrapper">
    <div id="marquee">
        Award-winning — Dedicated — Experienced — Passionate — 
    </div>
    <div id="marquee2">
        Award-winning — Dedicated — Experienced — Passionate — 
    </div>
</div>

<p>Some text here</p>
.marquee-wrapper {
    overflow: hidden;
    width: 400px;
    margin: 0 auto;
    border: 1px solid red;
}

And here is the JavaScript. Note that, after a while, it might get glitchy, especially if changing tabs. This is a browser thing, and there is nothing I can do about it. On the other hand, who spends more than 10-20 seconds looking at the scrolling text?

/**
 * Initialize a marquee effect for an HTML element.
 *
 * @param {HTMLElement} element - The element to apply the marquee effect to.
 * @param {number} [repeatCount=7] - The number of times to repeat the element's content.
 * @param {number} [step=1] - The step size for the marquee animation.
 */
const startMarquee = (element, repeatCount = 7, step = 1) => {
    /**
     * Function that animates the marquee effect.
     */
    const animateMarquee = () => {
        position = position < width ? position + step : 1;
        element.style.marginLeft = `${-position}px`;
        element.style.overflow = 'hidden';
        element.style.whiteSpace = 'nowrap';
        requestAnimationFrame(animateMarquee);
    };

    let position = 0;
    const space = '';
    const content = element.innerHTML;
    element.innerHTML = Array(repeatCount).fill(content + space).join('');
    element.style.position = 'absolute';
    const width = element.clientWidth + 1;
    element.style.position = '';

    // Start the marquee animation
    animateMarquee();
};

// Example usages:
startMarquee(document.getElementById('marquee'), 16, 0.5); // Customize repeatCount and step
startMarquee(document.getElementById('marquee2'), 16, 1);

See the Pen Marquee :: X6 by Ciprian (@ciprian) on CodePen.

JavaScript Marquee/Scrollable Text #4

The fourth example has a bit of interaction, and it stops when hovered.

Here is the HTML (note that you will need to add a separator yourself):

<div id="latest-news" class="marquee">
    <span style="white-space:nowrap;">
        <span>&nbsp;&bull;&nbsp;</span>
        <a href="#">Explore the hidden wonders of nature</a>
        <span>&nbsp;&bull;&nbsp;</span>
        <a href="#">Ignite your passion for creativity</a>
        <span>&nbsp;&bull;&nbsp;</span>
        <a href="#">Embrace the journey, not just the destination</a>
        <span>&nbsp;&bull;&nbsp;</span>
        <a href="#">Discover the magic in everyday moments</a>
        <span>&nbsp;&bull;&nbsp;</span>
        <a href="#">Chase your dreams with fearless enthusiasm</a>
    </span>
</div>

The CSS is, again, very simple — one selector:

.marquee {
    position: relative;
    overflow: hidden;
    text-align: center;
    margin: 0 auto;
    width: 100%;
    height: 30px; /* This is required, adjust as needed */
    display: flex;
    align-items: center;
    white-space: nowrap;
    font-size: 24px;
}

And here is the “heart” of the marquee, the JavaScript code:

function initializeMarquee() {
    createMarqueeContainer('latest-news');
    rotateMarquee(marqueeContainers);
}

window.onload = initializeMarquee;

function getObjectWidth(obj) {
    if (obj.offsetWidth) return obj.offsetWidth;
    if (obj.clip) return obj.clip.width;
    return 0;
}

const marqueeContainers = [];

function createMarqueeContainer(id) {
    const container = document.getElementById(id);
    const itemWidth = getObjectWidth(container.getElementsByTagName("span")[0]) + 5;
    const fullWidth = getObjectWidth(container);
    const textContent = container.getElementsByTagName("span")[0].innerHTML;
    container.innerHTML = "";
    const height = container.style.height;

    container.onmouseout = () => rotateMarquee(marqueeContainers);
    
    container.onmouseover = () => cancelAnimationFrame(marqueeContainers[0].animationID);

    container.items = [];
    const maxItems = Math.ceil(fullWidth / itemWidth) + 1;

    for (let i = 0; i < maxItems; i++) {
        container.items[i] = document.createElement("div");
        container.items[i].innerHTML = textContent;
        container.items[i].style.position = "absolute";
        container.items[i].style.left = itemWidth * i + "px";
        container.items[i].style.width = itemWidth + "px";
        container.items[i].style.height = height;
        container.appendChild(container.items[i]);
    }

    marqueeContainers.push(container);
}

function rotateMarquee(containers) {
    if (!containers) return;

    for (let j = containers.length - 1; j > -1; j--) {
        const maxItems = containers[j].items.length;

        for (let i = 0; i < maxItems; i++) {
            const itemStyle = containers[j].items[i].style;
            itemStyle.left = parseInt(itemStyle.left, 10) - 1 + "px";
        }

        const firstItemStyle = containers[j].items[0].style;

        if (parseInt(firstItemStyle.left, 10) + parseInt(firstItemStyle.width, 10) < 0) {
            const shiftedItem = containers[j].items.shift();
            shiftedItem.style.left = parseInt(shiftedItem.style.left) + parseInt(shiftedItem.style.width) * maxItems + "px";
            containers[j].items.push(shiftedItem);
        }
    }

    containers[0].animationID = requestAnimationFrame(() => rotateMarquee(containers));
}

Here is a demo:

See the Pen Simple javaScript marquee by Ciprian (@ciprian) on CodePen.

Use whichever one makes more sense for your set-up and for your content.

Related posts