{"id":6673,"date":"2025-08-04T13:30:28","date_gmt":"2025-08-04T18:30:28","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=6673"},"modified":"2025-08-04T13:30:29","modified_gmt":"2025-08-04T18:30:29","slug":"infinite-marquee-animation-using-modern-css","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/infinite-marquee-animation-using-modern-css\/","title":{"rendered":"Infinite Marquee Animation using Modern CSS"},"content":{"rendered":"\n<p>A set of logos with an infinite repeating slide animation is a classic component in web development. We can find countless examples and implementations starting from the old (and now deprecated) <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTML\/Reference\/Elements\/marquee\"><code>&lt;marquee&gt;<\/code> element<\/a>. I&#8217;ve written <a href=\"https:\/\/verpex.com\/blog\/website-tips\/how-to-create-a-css-only-infinite-scroll-animation\">an article<\/a> about it myself a few years ago.<\/p>\n\n\n\n<p><em>\u201cWhy another article?\u201d<\/em> you ask. CSS keeps evolving with new and powerful features, so I always try to find room for improvement and optimization. We&#8217;ll do that now with some new CSS features.<\/p>\n\n\n\n<p class=\"learn-more\">At the time of writing, only Chrome-based browsers have the full support of the features we will be using, which include features like <code>shape()<\/code>, <code>sibling-index()<\/code>, and <code>sibling-count()<\/code>.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_QwjGqEJ\" src=\"\/\/codepen.io\/anon\/embed\/QwjGqEJ?height=550&amp;theme-id=1&amp;slug-hash=QwjGqEJ&amp;default-tab=result\" height=\"550\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed QwjGqEJ\" title=\"CodePen Embed QwjGqEJ\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>In the demo above, we have an infinite marquee animation that works with <strong>any number<\/strong> of images. Simply add as many elements as you want in the HTML. There is no need to touch the CSS. You can easily control the number of visible images by adjusting one variable, and it\u2019s responsive. Resize the screen and see how things adjust smoothly.<\/p>\n\n\n\n<p>You might think the code is lengthy and full of complex calculations, but it\u2019s less than 10 lines of CSS with no JavaScript.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.container<\/span> {\n  <span class=\"hljs-attribute\">--s<\/span>: <span class=\"hljs-number\">150px<\/span>; <span class=\"hljs-comment\">\/* size of the images *\/<\/span>\n  <span class=\"hljs-attribute\">--d<\/span>: <span class=\"hljs-number\">8s<\/span>; <span class=\"hljs-comment\">\/* animation duration *\/<\/span>\n  <span class=\"hljs-attribute\">--n<\/span>: <span class=\"hljs-number\">4<\/span>; <span class=\"hljs-comment\">\/* number of visible images *\/<\/span>\n  \n  <span class=\"hljs-attribute\">display<\/span>: flex;\n  <span class=\"hljs-attribute\">overflow<\/span>: hidden;\n}\n<span class=\"hljs-selector-tag\">img<\/span> {\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-built_in\">var<\/span>(--s);\n  <span class=\"hljs-attribute\">offset<\/span>: <span class=\"hljs-built_in\">shape<\/span>(from calc(var(--s)\/-<span class=\"hljs-number\">2<\/span>) <span class=\"hljs-number\">50%<\/span>,hline by <span class=\"hljs-built_in\">calc<\/span>(sibling-count()*<span class=\"hljs-built_in\">max<\/span>(<span class=\"hljs-number\">100%<\/span>\/var(--n),<span class=\"hljs-built_in\">var<\/span>(--s))));\n  <span class=\"hljs-attribute\">animation<\/span>: x <span class=\"hljs-built_in\">var<\/span>(--d) linear infinite <span class=\"hljs-built_in\">calc<\/span>(-<span class=\"hljs-number\">1<\/span>*sibling-index()*<span class=\"hljs-built_in\">var<\/span>(--d)\/<span class=\"hljs-built_in\">sibling-count<\/span>());\n}\n<span class=\"hljs-keyword\">@keyframes<\/span> x { \n  <span class=\"hljs-selector-tag\">to<\/span> { <span class=\"hljs-attribute\">offset-distance<\/span>: <span class=\"hljs-number\">100%<\/span>; }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Perhaps this looks complex at first glance, especially that strange <code>offset<\/code> property! Don\u2019t stare too much at it; we will dissect it together, and by the end of the article, it will look quite easy.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-idea\">The Idea<\/h2>\n\n\n\n<p>The tricky part when creating a marquee is to have that cyclic animation where each element needs to \u201cjump\u201d to the beginning to slide again. Earlier implementations will duplicate the elements to simulate the infinite animation, but that\u2019s not a good approach as it requires you to manipulate the HTML, and you may have accessibility\/performance issues.<\/p>\n\n\n\n<p>Some modern implementations rely on a complex translate animation to create the \u201cjump\u201d of the element outside the visible area (the user doesn\u2019t see it) while having a continuous movement inside the visible area. This approach is perfect but requires some complex calculation and may depend on the number of elements you have in your HTML.<\/p>\n\n\n\n<p>It would be perfect if we could have a native way to create a continuous animation with the \u201cjump\u201d and, at the same time, make it work with any number of elements. The first part is doable and we don\u2019t need modern CSS for it. We can use <code>offset<\/code> combined with <code>path()<\/code> where the path will be a straight line.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_JoYEPaE\/83580353d67fc2a7c6a7140615d5228e\" src=\"\/\/codepen.io\/anon\/embed\/JoYEPaE\/83580353d67fc2a7c6a7140615d5228e?height=350&amp;theme-id=1&amp;slug-hash=JoYEPaE\/83580353d67fc2a7c6a7140615d5228e&amp;default-tab=css,result\" height=\"350\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed JoYEPaE\/83580353d67fc2a7c6a7140615d5228e\" title=\"CodePen Embed JoYEPaE\/83580353d67fc2a7c6a7140615d5228e\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Inside path, I am using the SVG syntax to define a line, and I simply move the image along that line by animating <code>offset-distance<\/code> between 0% and 100%. This looks perfect at first glance since we have the animation we want but it\u2019s not a flexible approach because <code>path()<\/code> accepts only hard-coded pixel values.<\/p>\n\n\n\n<p>To overcome the limitation of <code>path()<\/code>, we are going to use <a href=\"https:\/\/frontendmasters.com\/blog\/shape-a-new-powerful-drawing-syntax-in-css\/\">the new <code>shape()<\/code> function<\/a>! Here is a quote from <a href=\"https:\/\/drafts.csswg.org\/css-shapes-1\/#shape-function\">the specification<\/a>:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>The shape() function uses a set of commands roughly equivalent to the ones used by path(), but does so with more standard CSS syntax, and allows the full range of CSS functionality, such as additional units and math functions \u2026 In that sense, shape() is a superset of path().<\/p>\n<\/blockquote>\n\n\n\n<p>Instead of drawing a line using <code>path()<\/code>, we are going to use <code>shape()<\/code> to have the ability to rely on CSS and control the line based on the number of elements.<\/p>\n\n\n\n<p>Here is the previous demo using <code>shape()<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_jEbyEJx\/4133b73e6bc54832fe146960ccb54c74\" src=\"\/\/codepen.io\/anon\/embed\/jEbyEJx\/4133b73e6bc54832fe146960ccb54c74?height=350&amp;theme-id=1&amp;slug-hash=jEbyEJx\/4133b73e6bc54832fe146960ccb54c74&amp;default-tab=css,result\" height=\"350\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed jEbyEJx\/4133b73e6bc54832fe146960ccb54c74\" title=\"CodePen Embed jEbyEJx\/4133b73e6bc54832fe146960ccb54c74\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>If you are unfamiliar with <code>shape()<\/code>, don\u2019t worry. Our use case is pretty basic as we are going to simply draw a horizontal line using the following syntax:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">offset: shape(from X Y, hline by length);<\/pre>\n\n\n\n<p>The goal is to find the <code>X Y<\/code> values (the coordinates of the starting point) and the <code>length<\/code> value (the length of the line).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-implementation\">The Implementation<\/h2>\n\n\n\n<p>Let\u2019s start with the HTML structure, which is a set of images inside a container:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"container\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">img<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">\"\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">img<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">\"\"<\/span>&gt;<\/span>\n  <span class=\"hljs-comment\">&lt;!-- as many images as you want --&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We make the container flexbox to remove the default space between the image and make sure they don\u2019t wrap even if the container is smaller (remember that <code>flex-wrap<\/code> is by default <code>nowrap<\/code>).<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_RNWKPNK\/7558821ff8693b58642b1a6fb8e4de42\" src=\"\/\/codepen.io\/anon\/embed\/RNWKPNK\/7558821ff8693b58642b1a6fb8e4de42?height=450&amp;theme-id=1&amp;slug-hash=RNWKPNK\/7558821ff8693b58642b1a6fb8e4de42&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed RNWKPNK\/7558821ff8693b58642b1a6fb8e4de42\" title=\"CodePen Embed RNWKPNK\/7558821ff8693b58642b1a6fb8e4de42\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Now, let\u2019s suppose we want to see only N images at a time. For this, we need to define the width of the container to be equal to <code>N x size_of_image<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.container<\/span> {\n  <span class=\"hljs-attribute\">--s<\/span>: <span class=\"hljs-number\">100px<\/span>; <span class=\"hljs-comment\">\/* size of the image *\/<\/span>\n  <span class=\"hljs-attribute\">--n<\/span>: <span class=\"hljs-number\">4<\/span>; <span class=\"hljs-comment\">\/* number of visible images *\/<\/span>\n\n  <span class=\"hljs-attribute\">display<\/span>: flex;\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-built_in\">calc<\/span>(var(--n) * <span class=\"hljs-built_in\">var<\/span>(--s));\n  <span class=\"hljs-attribute\">overflow<\/span>: hidden;\n}\n<span class=\"hljs-selector-tag\">img<\/span> {\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-built_in\">var<\/span>(--s);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_qEORdbO\/5c9a2636d2c98b03f16d8d55a2a78528\" src=\"\/\/codepen.io\/anon\/embed\/qEORdbO\/5c9a2636d2c98b03f16d8d55a2a78528?height=250&amp;theme-id=1&amp;slug-hash=qEORdbO\/5c9a2636d2c98b03f16d8d55a2a78528&amp;default-tab=result\" height=\"250\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed qEORdbO\/5c9a2636d2c98b03f16d8d55a2a78528\" title=\"CodePen Embed qEORdbO\/5c9a2636d2c98b03f16d8d55a2a78528\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Nothing complex so far. We introduced some variables to control the size and the number of visible images. Now let\u2019s move to the animation.<\/p>\n\n\n\n<p>To have a continuous animation, the length of the line needs to be equal to the total number of images multiplied by the size of one image. In other words, we should have a line that can contain all the images side by side. The <code>offset<\/code> property is defined on the image elements, and thanks to modern CSS, we can rely on the new <code>sibling-count()<\/code> to get the total number of images.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">offset: shape(<span class=\"hljs-keyword\">from<\/span> X Y, hline by calc(sibling-count() * <span class=\"hljs-keyword\">var<\/span>(--s)));<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>What about the <code>X Y<\/code> values? Let\u2019s try <code>0 0<\/code> and see what happens:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_azvpOBO\/c0fa6a1b39b58ef8120b547c75171538\" src=\"\/\/codepen.io\/anon\/embed\/azvpOBO\/c0fa6a1b39b58ef8120b547c75171538?height=250&amp;theme-id=1&amp;slug-hash=azvpOBO\/c0fa6a1b39b58ef8120b547c75171538&amp;default-tab=result\" height=\"250\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed azvpOBO\/c0fa6a1b39b58ef8120b547c75171538\" title=\"CodePen Embed azvpOBO\/c0fa6a1b39b58ef8120b547c75171538\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Hmm, not quite good. All the images are above each other, and their position is a bit off. The first issue is logical since they share the same animation. We will fix it later by introducing a delay.<\/p>\n\n\n\n<p>The trickiest part when working with <code>offset<\/code> is defining the position. The property is applied on the child elements (the images in our case), but the reference is the parent container. By specifying <code>0 0<\/code>, we are considering the top-left corner of the parent as the starting point of the line.<\/p>\n\n\n\n<p>What about the images? How are they placed? If you remove the animation and keep the <code>offset-distance<\/code> equal to 0% (the default value), you will see the following.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"577\" height=\"258\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/GsbSeQsh.png?resize=577%2C258&#038;ssl=1\" alt=\"An animated marquee with text that moves horizontally across a container, showcasing a modern CSS implementation for infinite scrolling images or text.\" class=\"wp-image-6675\" style=\"width:508px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/GsbSeQsh.png?w=577&amp;ssl=1 577w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/GsbSeQsh.png?resize=300%2C134&amp;ssl=1 300w\" sizes=\"auto, (max-width: 577px) 100vw, 577px\" \/><\/figure>\n<\/div>\n\n\n<p>The center of the images is placed at the <code>0 0<\/code>, and starting from there, they move horizontally until the end of the line. Let\u2019s update the <code>X Y<\/code> values to rectify the position of the line and bring the images inside the container. For this, the line needs to be in the middle <code>0 50%<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">offset: shape(from 0 50%, hline by calc(sibling-count() * var(--s)));<\/pre>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_YPyNXEQ\/84498c436d373370365d3117b8f574e8\" src=\"\/\/codepen.io\/anon\/embed\/YPyNXEQ\/84498c436d373370365d3117b8f574e8?height=250&amp;theme-id=1&amp;slug-hash=YPyNXEQ\/84498c436d373370365d3117b8f574e8&amp;default-tab=result\" height=\"250\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed YPyNXEQ\/84498c436d373370365d3117b8f574e8\" title=\"CodePen Embed YPyNXEQ\/84498c436d373370365d3117b8f574e8\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>It\u2019s better, and we can already see the continuous animation. It\u2019s still not perfect because we can see the \u201cjump\u201d of the image on the left. We need to update the position of the line so it starts outside the container and we don\u2019t see the \u201cjump\u201d of the images. The <code>X<\/code> value should be equal to <code>-S\/2<\/code> instead of 0.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">offset: shape(from calc(var(--s)\/-2) 50%, hline by calc(sibling-count() * var(--s)));<\/pre>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_RNWKPQj\/fd20580dbb66db078fa2b563ced07c5f\" src=\"\/\/codepen.io\/anon\/embed\/RNWKPQj\/fd20580dbb66db078fa2b563ced07c5f?height=250&amp;theme-id=1&amp;slug-hash=RNWKPQj\/fd20580dbb66db078fa2b563ced07c5f&amp;default-tab=result\" height=\"250\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed RNWKPQj\/fd20580dbb66db078fa2b563ced07c5f\" title=\"CodePen Embed RNWKPQj\/fd20580dbb66db078fa2b563ced07c5f\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>No more visible jump, the animation is perfect!<\/p>\n\n\n\n<p>To fix the overlap between the images, we need to consider a different delay for each image. We can use <code>nth-child()<\/code> to select each image individually and define the delay following the logic below:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">img<\/span><span class=\"hljs-selector-pseudo\">:nth-child(1)<\/span> {<span class=\"hljs-attribute\">animation-delay<\/span>: -<span class=\"hljs-number\">1<\/span> *  duration\/total_image }\n<span class=\"hljs-selector-tag\">img<\/span><span class=\"hljs-selector-pseudo\">:nth-child(2)<\/span> {<span class=\"hljs-attribute\">animation-delay<\/span>: -<span class=\"hljs-number\">2<\/span> *  duration\/total_image }\n<span class=\"hljs-comment\">\/* and so on *\/<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Tedious work, right? And we need as many selectors as the number of images in the HTML code, which is not good. What we want is a generic CSS code that doesn\u2019t depend on the HTML structure (the number of images).<\/p>\n\n\n\n<p>Similar to the <code>sibling-count()<\/code>that gives us the total number of images, we also have <code><a href=\"https:\/\/css-tip.com\/element-index\/\">sibling-index()<\/a><\/code> <a href=\"https:\/\/css-tip.com\/element-index\/\">that gives us the index of each image within the container<\/a>. All we have to do is to update the animation property and include the delay using the index value that will be different for each image, hence a different delay for each image!<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">animation<\/span>: \n  <span class=\"hljs-selector-tag\">x<\/span> <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--d<\/span>) <span class=\"hljs-selector-tag\">linear<\/span> <span class=\"hljs-selector-tag\">infinite<\/span> \n  <span class=\"hljs-selector-tag\">calc<\/span>(<span class=\"hljs-selector-tag\">-1<\/span>*<span class=\"hljs-selector-tag\">sibling-index<\/span>()*<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--d<\/span>)\/<span class=\"hljs-selector-tag\">sibling-count<\/span>());<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_PwPWqda\/090b83479932a66225d8390adefdfe93\" src=\"\/\/codepen.io\/anon\/embed\/PwPWqda\/090b83479932a66225d8390adefdfe93?height=250&amp;theme-id=1&amp;slug-hash=PwPWqda\/090b83479932a66225d8390adefdfe93&amp;default-tab=result\" height=\"250\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed PwPWqda\/090b83479932a66225d8390adefdfe93\" title=\"CodePen Embed PwPWqda\/090b83479932a66225d8390adefdfe93\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Everything is perfect! The final code is as follows:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.container<\/span> {\n  <span class=\"hljs-attribute\">--s<\/span>: <span class=\"hljs-number\">100px<\/span>; <span class=\"hljs-comment\">\/* size of the image *\/<\/span>\n  <span class=\"hljs-attribute\">--d<\/span>: <span class=\"hljs-number\">4s<\/span>; <span class=\"hljs-comment\">\/* animation duration *\/<\/span>\n  <span class=\"hljs-attribute\">--n<\/span>: <span class=\"hljs-number\">4<\/span>; <span class=\"hljs-comment\">\/* number of visible images *\/<\/span>\n  \n  <span class=\"hljs-attribute\">display<\/span>: flex;\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-built_in\">calc<\/span>(var(--n) * <span class=\"hljs-built_in\">var<\/span>(--s));\n  <span class=\"hljs-attribute\">overflow<\/span>: hidden;\n}\n<span class=\"hljs-selector-tag\">img<\/span> {\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-built_in\">var<\/span>(--s);\n  <span class=\"hljs-attribute\">offset<\/span>: <span class=\"hljs-built_in\">shape<\/span>(from calc(var(--s)\/-<span class=\"hljs-number\">2<\/span>) <span class=\"hljs-number\">50%<\/span>, hline by <span class=\"hljs-built_in\">calc<\/span>(sibling-count() * <span class=\"hljs-built_in\">var<\/span>(--s)));\n  <span class=\"hljs-attribute\">animation<\/span>: x <span class=\"hljs-built_in\">var<\/span>(--d) linear infinite <span class=\"hljs-built_in\">calc<\/span>(-<span class=\"hljs-number\">1<\/span>*sibling-index()*<span class=\"hljs-built_in\">var<\/span>(--d)\/<span class=\"hljs-built_in\">sibling-count<\/span>());\n}\n<span class=\"hljs-keyword\">@keyframes<\/span> x { \n  <span class=\"hljs-selector-tag\">to<\/span> {<span class=\"hljs-attribute\">offset-distance<\/span>: <span class=\"hljs-number\">100%<\/span>}\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We barely have 10 lines of CSS with no hardcoded values or magic numbers!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"lets-make-it-responsive\">Let\u2019s Make it Responsive<\/h2>\n\n\n\n<p>In the previous example, we fixed the width of the container to accommodate the number of images we want to show but what about a responsive behavior where the container width is unknown? We want to show only N images at a time within a container that doesn\u2019t have a fixed width.<\/p>\n\n\n\n<p>The observation we can make is that if the container width is bigger than <code>NxS<\/code>, we will have space between images, which means that the line defined by <code>shape()<\/code> needs to be longer as it should contain the extra space. The goal is to find the new length of the line.<\/p>\n\n\n\n<p>Having N images visible at a time means that we can express the width of the container as follows:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">width = N x (image_size + space_around_image)<\/pre>\n\n\n\n<p>We know the size of the image and N (Defined by <code>--s<\/code> and <code>--n<\/code>), so the space will depend on the container width. The bigger the container is, the more space we have. That space needs to be included in the length of the line.<\/p>\n\n\n\n<p>Instead of:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">hline by calc(sibling-count() * var(--s))<\/pre>\n\n\n\n<p>We need to use:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">hline by calc(sibling-count() * (var(--s) + space_around_image))<\/pre>\n\n\n\n<p>We use the formula of the container width and replace <code>(var(--s) + space_around_image)<\/code> with <code>width \/ var(--n)<\/code> and get the following:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">hline by calc(sibling-count() * width \/ var(--n) )<\/pre>\n\n\n\n<p>Hmm, what about the width value? It\u2019s unknown, so how do we find it?<\/p>\n\n\n\n<p>The width is nothing but <code>100%<\/code>! Remember that <code>offset<\/code> considers the parent container as the reference for its calculation so <code>100%<\/code> is relative to the parent dimension. We are drawing a horizontal line thus <code>100%<\/code> will resolve to the container width.<\/p>\n\n\n\n<p>The new offset value will be equal to:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">shape(from calc(var(--s)\/-2) 50%, hline by calc(sibling-count() * 100% \/ var(--n)));<\/pre>\n\n\n\n<p>And our animation is now responsive.<\/p>\n\n\n\n<p>Resize the container (or the screen) in the below demo and see the magic in play:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_zxvNvrN\/4057a6b781169984aff772b64cad1c72\" src=\"\/\/codepen.io\/anon\/embed\/zxvNvrN\/4057a6b781169984aff772b64cad1c72?height=250&amp;theme-id=1&amp;slug-hash=zxvNvrN\/4057a6b781169984aff772b64cad1c72&amp;default-tab=result\" height=\"250\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed zxvNvrN\/4057a6b781169984aff772b64cad1c72\" title=\"CodePen Embed zxvNvrN\/4057a6b781169984aff772b64cad1c72\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>We have the responsive part but it\u2019s still not perfect because if the container is too small, the images will overlap each other.<\/p>\n\n\n\n<p>We can fix this by combining the new code with the previous one. The idea is to make sure the length of the line is at least equal to the total number of images multiplied by the size of one image. Remember, it\u2019s the length that allows all the images to be contained within the line without overlap.<\/p>\n\n\n\n<p>So we update the following part:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">calc(sibling-count() * 100%\/var(--n))<\/pre>\n\n\n\n<p>With:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">max(sibling-count() * 100%\/var(--n), sibling-count() * var(--s))<\/pre>\n\n\n\n<p>The first argument of <code>max()<\/code> is the responsive length, and the second one is the fixed length. If the first value is smaller than the second, we will use the latter and the images will not overlap.<\/p>\n\n\n\n<p>We can still optimize the code a little as follows:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">calc(sibling-count() * max(100%\/var(--n),var(--s)))<\/pre>\n\n\n\n<p>We can also add a small amount to the fixed length that will play the role of the minimum gap between images and prevent them from touching each other:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">calc(sibling-count() * max(100%\/var(--n),var(--s) + 10px))<\/pre>\n\n\n\n<p>We are done! A fully responsive marquee animation using modern CSS.<\/p>\n\n\n\n<p>Here is again the demo I shared at the beginning of the article with all the adjustments made:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_QwjGqEJ\" src=\"\/\/codepen.io\/anon\/embed\/QwjGqEJ?height=550&amp;theme-id=1&amp;slug-hash=QwjGqEJ&amp;default-tab=result\" height=\"550\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed QwjGqEJ\" title=\"CodePen Embed QwjGqEJ\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Do you still see the code as a complex one? I hope not!<\/p>\n\n\n\n<p class=\"learn-more\">The use of <code>min()<\/code> or <code>max()<\/code> is not always intuitive, but <a href=\"https:\/\/css-tip.com\/min-max\/\">I have a small tutorial that can help you identify which one to use<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"more-examples\">More Examples<\/h2>\n\n\n\n<p>I used images to explain the technique, but we can easily extend it to any kind of content. The only requirement\/limitation is to have equal-width items.<\/p>\n\n\n\n<p>We can have some text animations:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_vENyewr\" src=\"\/\/codepen.io\/anon\/embed\/vENyewr?height=350&amp;theme-id=1&amp;slug-hash=vENyewr&amp;default-tab=result\" height=\"350\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed vENyewr\" title=\"CodePen Embed vENyewr\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Or more complex elements with image + text:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_vENyead\" src=\"\/\/codepen.io\/anon\/embed\/vENyead?height=350&amp;theme-id=1&amp;slug-hash=vENyead&amp;default-tab=result\" height=\"350\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed vENyead\" title=\"CodePen Embed vENyead\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>In both examples, I am using <code>flex-shrink: 0<\/code> to avoid the default shrinking effect of the flex items when the container gets smaller. We didn\u2019t have this issue with images as they won\u2019t shrink past their defined size.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"conclusion\">Conclusion<\/h2>\n\n\n\n<p>Some of you will probably never need a marquee animation, but it was a good opportunity to explore modern features that can be useful such as the <code>shape()<\/code> and the <code>sibling-*()<\/code> functions. Not to mention the use of CSS variables, <code>calc()<\/code>, <code>max()<\/code>, etc., which I still consider part of modern CSS even if they are more common.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A row of logos that animate forever perfectly and don&#8217;t have any duplicated HTML or JavaScript at all is quite a trick. Thanks modern CSS! <\/p>\n","protected":false},"author":12,"featured_media":6698,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[100,341,7,250,287],"class_list":["post-6673","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-animation","tag-carousels","tag-css","tag-sibling-index","tag-slider"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/logos.jpg?fit=2100%2C1300&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6673","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/12"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=6673"}],"version-history":[{"count":5,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6673\/revisions"}],"predecessor-version":[{"id":6700,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6673\/revisions\/6700"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/6698"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=6673"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=6673"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=6673"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}