CSS Animation Example: Infinite Scrolling Marquee

The "Infinite Scrolling Marquee" is heavily utilized in B2B landing pages to showcase a massive row of client logos that slides endlessly across the screen.

Logo 1
Logo 2
Logo 3
Logo 4
Logo 5

The "trick" to an infinite seamless CSS loop is mathematically deceiving the user's eye by duplicating the contents. Inside the track, you must have two identical sets of items.

The track is allowed to stretch infinitely wide using width: max-content. The @keyframes simply translates the entire track continuously to the left using transform: translateX(). Importantly, the animation must stop exactly exactly when the track has moved left by -50% of its width.

Because the first half and the second half of the track are identical, by the time it reaches -50%, the first element of duplicate set "B" is now sitting in the exact pixel position where the first element of set "A" started. The animation loops back to 0% invisibly, resetting the loop.

Here is the code to create the Infinite Scrolling Marquee.

View Output

Accessibility Considerations

Continuous automated motion that the user cannot control is bad practice for readability. We mitigate this partly by setting animation-play-state: paused on the track when the container is hovered or receiving focus.

Furthermore, prefers-reduced-motion is a CSS media feature used to detect if a user has enabled a setting on their device to minimize the amount of non-essential animation or motion. It is recommended to destroy the marquee functionality entirely for these users. By disabling the scrolling animation and applying flex-wrap: wrap on the track, the logos simply stack neatly like a normal grid. We also recommend leveraging the aria-hidden="true" attribute to not only hide the duplicates from screen readers, but also from view for users preferring reduced motion.

Here's what we used in our example:


@media (prefers-reduced-motion: reduce) {
    .marquee-track {
        /* Stops the scrolling */
        animation: none; 
        
        /* Allows the items to stack conventionally rather than bursting out wide */
        flex-wrap: wrap; 
        width: 100%;
        justify-content: center;
    }
    .marquee-item[aria-hidden="true"] {
        /* Hides the duplicate set */
        display: none;
    }
}