A pure-CSS take on the endless logo carousel
If a design calls for a horizontal auto-scrolling strip of logos, the first instinct might be to reach for the retired <marquee> element. That would be a mistake. MDN is unambiguous: the element is deprecated, may no longer be supported, and should not be used in new code. Fortunately, everything needed for that effect already exists in CSS, and it can be done with very little markup — just a container and the images themselves.
The goal is a line of logos that scrolls endlessly, so that when the final image enters the frame, the first one follows directly behind it without a visible seam. The approach below keeps the HTML flat, avoids JavaScript entirely, and relies on a small set of CSS variables to make the positioning and timing work.
Building the container
The structure is deliberately minimal. A single container holds all the logo images, and no wrappers or duplicate sets of items are required.
<figure class="marquee">
<img class="marquee__item" src="logo-1.png" width="100" height="100" alt="Company 1">
<img class="marquee__item" src="logo-2.png" width="100" height="100" alt="Company 2">
<img class="marquee__item" src="logo-3.png" width="100" height="100" alt="Company 3">
</figure>
From there, the container does most of the work. Flexbox is a natural fit for laying out the images in a row with a gap, though the default row direction means no explicit flex-direction is needed.
.marquee {
display: flex;
}
Because the images will be pulled out of the document flow with absolute positioning, the container needs relative positioning to act as their containing block. An explicit height is also required, since the absolutely positioned children contribute no space of their own. A maximum width keeps the sliding boundaries under control. CSS variables make these values easier to manage later.
.marquee {
--marquee-max-width: 90vw;
display: flex;
block-size: var(--marquee-item-height);
max-inline-size: var(--marquee-max-width);
position: relative;
}
Next, the container needs to hide any image that slides outside its boundaries. The horizontal overflow is clipped, and a CSS mask is applied so the logos fade gently at the edges rather than disappearing abruptly. The masking trick, seen in other logo-wall implementations, gives the effect a more polished finish.
.marquee {
--marquee-max-width: 90vw;
display: flex;
block-size: var(--marquee-item-height);
max-inline-size: var(--marquee-max-width);
overflow-x: hidden;
position: relative;
}
.marquee {
display: flex;
block-size: var(--marquee-item-height);
max-inline-size: var(--marquee-max-width);
overflow-x: hidden;
position: relative;
mask-image: linear-gradient(
to right,
hsl(0 0% 0% / 0),
hsl(0 0% 0% / 1) 20%,
hsl(0 0% 0% / 1) 80%,
hsl(0 0% 0% / 0)
);
position: relative;
}
Positioning the items
With the container in place, the images are taken out of the flow entirely. They are absolutely positioned at the top-left corner, which stacks them on top of each other. A local variable ties each item's height to the container's declared height.
.marquee__item {
position: absolute;
}
.marquee__item {
position: absolute;
inset-inline-start: var(--marquee-item-offset);
}
Each item then needs to be shifted to the right, outside the visible area, so the animation can bring it in. The offset has to account for both the container width and the combined width of all logos. A static value would break on differently sized screens, so the calculation combines two ideas: the total width of all items plus the gap between them, and the container's maximum width plus a single logo width. The max() function picks the larger of the two, preventing items from overlapping when the viewport is narrow and keeping them out of view on wide displays.
calc(var(--marquee-item-width) * var(--marquee-items))
--marquee-item-offset: max(
calc(var(--marquee-item-width) * var(--marquee-items)),
calc(100% + var(--marquee-item-width))
);
Animation and delay
The animation itself is a linear horizontal translation over a keyframe based on the width of one logo. The position moves the item from right to left, and the loop runs forever.
@keyframes go {
to {
inset-inline-start: calc(var(--marquee-item-width) * -1);
}
}
The delay is the least intuitive part. It is derived from the animation duration and the item's index within the sequence, using a quadratic expression so each item lines up correctly in the stream. A negative delay makes the animation begin as if it had already been running.
--marquee-delay: calc(var(--marquee-duration) / var(--marquee-items) * (var(--marquee-items) - var(--marquee-item-index)) * -1);
var(--marquee-items) * (var(--marquee-items) - var(--marquee-item-index))
All the configuration lives in a set of variables, including the number of items and the dimensions of the logos. A modifier class, such as .marquee--8 for the eight-logo example, provides the specific values.
.marquee--8 {
--marquee-item-width: 100px;
--marquee-item-height: 100px;
--marquee-duration: 36s;
--marquee-items: 8;
}
Setting the index for each item is the last step before the loop is complete.
.marquee--8 .marquee__item:nth-of-type(1) {
--marquee-item-index: 1;
}
.marquee--8 .marquee__item:nth-of-type(2) {
--marquee-item-index: 2;
}
/* etc. */
.marquee--8 .marquee__item:nth-of-type(8) {
--marquee-item-index: 8;
}
The full effect is visible in the demo below.
See the Pen [CSS only marquee without HTML duplication [forked]](https://codepen.io/smashingmag/pen/xxerNKz) by Silvestar Bistrović.
Reducing motion
A constant horizontal scroll can be a problem for people with vestibular disorders, so the animation should respect the user's preference for reduced motion.
@media (prefers-reduced-motion) {
.marquee__item {
animation-play-state: paused;
}
}
That alone might not be enough. Turning the animation off means the logos sit in the offset position, outside the visible container. A further adjustment ensures more of the strip is revealed when motion is disabled, so the content remains useful.
@media (prefers-reduced-motion) {
.marquee {
justify-content: space-evenly;
mask-image: unset;
}
.marquee__item {
position: unset;
inset-inline-start: unset;
transform: unset;
}
@keyframes go {
to {
inset-inline-start: unset;
}
}
}
For a more deliberate control, a play/pause button is the more robust option, but the level of effort depends on whether the animation is central to the interface or purely decorative.
Remaining refinements
This technique is solid, but it is not perfect. When logos have uneven widths, the gap between items as they traverse the loop can drift, and a precise formula to correct that still needs tuning. On very wide screens, large empty margins can appear; constraining the container and centering it with margin-inline: auto addresses that case.
See the Pen [CSS only marquee without HTML duplication, example 5 [forked]](https://codepen.io/smashingmag/pen/qBwjGBJ) by Silvestar Bistrović.
With a clean HTML skeleton and no script requirements, the pattern is easy to drop into a project and adapt to its specific dimensions and brand assets.



