Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Mon, 04 Aug 2025 18:30:29 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.3 225069128 Infinite Marquee Animation using Modern CSS https://frontendmasters.com/blog/infinite-marquee-animation-using-modern-css/ https://frontendmasters.com/blog/infinite-marquee-animation-using-modern-css/#comments Mon, 04 Aug 2025 18:30:28 +0000 https://frontendmasters.com/blog/?p=6673 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) <marquee> element. I’ve written an article about it myself a few years ago.

“Why another article?” you ask. CSS keeps evolving with new and powerful features, so I always try to find room for improvement and optimization. We’ll do that now with some new CSS features.

At the time of writing, only Chrome-based browsers have the full support of the features we will be using, which include features like shape(), sibling-index(), and sibling-count().

In the demo above, we have an infinite marquee animation that works with any number 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’s responsive. Resize the screen and see how things adjust smoothly.

You might think the code is lengthy and full of complex calculations, but it’s less than 10 lines of CSS with no JavaScript.

.container {
  --s: 150px; /* size of the images */
  --d: 8s; /* animation duration */
  --n: 4; /* number of visible images */
  
  display: flex;
  overflow: hidden;
}
img {
  width: var(--s);
  offset: shape(from calc(var(--s)/-2) 50%,hline by calc(sibling-count()*max(100%/var(--n),var(--s))));
  animation: x var(--d) linear infinite calc(-1*sibling-index()*var(--d)/sibling-count());
}
@keyframes x { 
  to { offset-distance: 100%; }
}

Perhaps this looks complex at first glance, especially that strange offset property! Don’t stare too much at it; we will dissect it together, and by the end of the article, it will look quite easy.

The Idea

The tricky part when creating a marquee is to have that cyclic animation where each element needs to “jump” to the beginning to slide again. Earlier implementations will duplicate the elements to simulate the infinite animation, but that’s not a good approach as it requires you to manipulate the HTML, and you may have accessibility/performance issues.

Some modern implementations rely on a complex translate animation to create the “jump” of the element outside the visible area (the user doesn’t 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.

It would be perfect if we could have a native way to create a continuous animation with the “jump” and, at the same time, make it work with any number of elements. The first part is doable and we don’t need modern CSS for it. We can use offset combined with path() where the path will be a straight line.

Inside path, I am using the SVG syntax to define a line, and I simply move the image along that line by animating offset-distance between 0% and 100%. This looks perfect at first glance since we have the animation we want but it’s not a flexible approach because path() accepts only hard-coded pixel values.

To overcome the limitation of path(), we are going to use the new shape() function! Here is a quote from the specification:

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 … In that sense, shape() is a superset of path().

Instead of drawing a line using path(), we are going to use shape() to have the ability to rely on CSS and control the line based on the number of elements.

Here is the previous demo using shape():

If you are unfamiliar with shape(), don’t worry. Our use case is pretty basic as we are going to simply draw a horizontal line using the following syntax:

offset: shape(from X Y, hline by length);

The goal is to find the X Y values (the coordinates of the starting point) and the length value (the length of the line).

The Implementation

Let’s start with the HTML structure, which is a set of images inside a container:

<div class="container">
  <img src="">
  <img src="">
  <!-- as many images as you want -->
</div>

We make the container flexbox to remove the default space between the image and make sure they don’t wrap even if the container is smaller (remember that flex-wrap is by default nowrap).

Now, let’s 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 N x size_of_image.

.container {
  --s: 100px; /* size of the image */
  --n: 4; /* number of visible images */

  display: flex;
  width: calc(var(--n) * var(--s));
  overflow: hidden;
}
img {
  width: var(--s);
}

Nothing complex so far. We introduced some variables to control the size and the number of visible images. Now let’s move to the animation.

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 offset property is defined on the image elements, and thanks to modern CSS, we can rely on the new sibling-count() to get the total number of images.

offset: shape(from X Y, hline by calc(sibling-count() * var(--s)));

What about the X Y values? Let’s try 0 0 and see what happens:

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.

The trickiest part when working with offset 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 0 0, we are considering the top-left corner of the parent as the starting point of the line.

What about the images? How are they placed? If you remove the animation and keep the offset-distance equal to 0% (the default value), you will see the following.

An animated marquee with text that moves horizontally across a container, showcasing a modern CSS implementation for infinite scrolling images or text.

The center of the images is placed at the 0 0, and starting from there, they move horizontally until the end of the line. Let’s update the X Y 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 0 50%.

offset: shape(from 0 50%, hline by calc(sibling-count() * var(--s)));

It’s better, and we can already see the continuous animation. It’s still not perfect because we can see the “jump” of the image on the left. We need to update the position of the line so it starts outside the container and we don’t see the “jump” of the images. The X value should be equal to -S/2 instead of 0.

offset: shape(from calc(var(--s)/-2) 50%, hline by calc(sibling-count() * var(--s)));

No more visible jump, the animation is perfect!

To fix the overlap between the images, we need to consider a different delay for each image. We can use nth-child() to select each image individually and define the delay following the logic below:

img:nth-child(1) {animation-delay: -1 *  duration/total_image }
img:nth-child(2) {animation-delay: -2 *  duration/total_image }
/* and so on */

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’t depend on the HTML structure (the number of images).

Similar to the sibling-count()that gives us the total number of images, we also have sibling-index() that gives us the index of each image within the container. 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!

animation: 
  x var(--d) linear infinite 
  calc(-1*sibling-index()*var(--d)/sibling-count());

Everything is perfect! The final code is as follows:

.container {
  --s: 100px; /* size of the image */
  --d: 4s; /* animation duration */
  --n: 4; /* number of visible images */
  
  display: flex;
  width: calc(var(--n) * var(--s));
  overflow: hidden;
}
img {
  width: var(--s);
  offset: shape(from calc(var(--s)/-2) 50%, hline by calc(sibling-count() * var(--s)));
  animation: x var(--d) linear infinite calc(-1*sibling-index()*var(--d)/sibling-count());
}
@keyframes x { 
  to {offset-distance: 100%}
}

We barely have 10 lines of CSS with no hardcoded values or magic numbers!

Let’s Make it Responsive

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’t have a fixed width.

The observation we can make is that if the container width is bigger than NxS, we will have space between images, which means that the line defined by shape() needs to be longer as it should contain the extra space. The goal is to find the new length of the line.

Having N images visible at a time means that we can express the width of the container as follows:

width = N x (image_size + space_around_image)

We know the size of the image and N (Defined by --s and --n), 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.

Instead of:

hline by calc(sibling-count() * var(--s))

We need to use:

hline by calc(sibling-count() * (var(--s) + space_around_image))

We use the formula of the container width and replace (var(--s) + space_around_image) with width / var(--n) and get the following:

hline by calc(sibling-count() * width / var(--n) )

Hmm, what about the width value? It’s unknown, so how do we find it?

The width is nothing but 100%! Remember that offset considers the parent container as the reference for its calculation so 100% is relative to the parent dimension. We are drawing a horizontal line thus 100% will resolve to the container width.

The new offset value will be equal to:

shape(from calc(var(--s)/-2) 50%, hline by calc(sibling-count() * 100% / var(--n)));

And our animation is now responsive.

Resize the container (or the screen) in the below demo and see the magic in play:

We have the responsive part but it’s still not perfect because if the container is too small, the images will overlap each other.

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’s the length that allows all the images to be contained within the line without overlap.

So we update the following part:

calc(sibling-count() * 100%/var(--n))

With:

max(sibling-count() * 100%/var(--n), sibling-count() * var(--s))

The first argument of max() 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.

We can still optimize the code a little as follows:

calc(sibling-count() * max(100%/var(--n),var(--s)))

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:

calc(sibling-count() * max(100%/var(--n),var(--s) + 10px))

We are done! A fully responsive marquee animation using modern CSS.

Here is again the demo I shared at the beginning of the article with all the adjustments made:

Do you still see the code as a complex one? I hope not!

The use of min() or max() is not always intuitive, but I have a small tutorial that can help you identify which one to use.

More Examples

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.

We can have some text animations:

Or more complex elements with image + text:

In both examples, I am using flex-shrink: 0 to avoid the default shrinking effect of the flex items when the container gets smaller. We didn’t have this issue with images as they won’t shrink past their defined size.

Conclusion

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 shape() and the sibling-*() functions. Not to mention the use of CSS variables, calc(), max(), etc., which I still consider part of modern CSS even if they are more common.

]]>
https://frontendmasters.com/blog/infinite-marquee-animation-using-modern-css/feed/ 6 6673
Quantity Query Carousel https://frontendmasters.com/blog/quantity-query-carousel/ https://frontendmasters.com/blog/quantity-query-carousel/#comments Wed, 25 Jun 2025 23:04:06 +0000 https://frontendmasters.com/blog/?p=6323 The concept of a quantity query is really neat. Coined by Heydon back in 2015, the idea is that you apply different styles depending on how many siblings there are. They was a way to do it back then, but it’s gotten much easier thanks to :has(), which not only makes the detection easier but gives us access to the parent element where we likely want it.

For instance:

.grid {
  display: grid;

  &:has(:nth-child(2)) {
    /* Has at least 2 elements */
    grid-template-columns: 1fr 1fr;
  }
 
  /* Use a :not() to do reverse logic */
}

What if we kept going with the idea where we…

  • If there is 1 element, let it be full-width
  • If there are 2 elements, set them side-by-side
  • If there are 3 elements, the first two are side-by-side, then the last is full-width
  • If there are 4 elements, then it’s a 2×2 grid

Then…

  • If there are 5+ elements, woah there, let’s just make it a carousel.

I heard Ahmad Shadeed mention this idea on stage at CSS Day and I had to try it myself. Good news is that it works, particularly if you can stomach the idea of a “carousel” just being “horizontal overflow with some scroll snapping” in Firefox/Safari for now. Of course you’d be free to make your own fallback as needed.

Here’s the whole gang:

Setup & One

The default setup can be something like:

.grid {
  display: grid;
  gap: 1rem;
}

Honestly we don’t even really need to make it a grid for one item, but it doesn’t really hurt and now we’re set up for the rest of them.

Two

Does it have two? Yeah? Let’s do this.

.grid {
  ...

  &:has(:nth-child(2)) {
    grid-template-columns: 1fr 1fr;
  }
}

Note that if our grid has three or more elements, this will also match. So if want to do something different with columns, we’ll need to override this or otherwise change things.

Three

To illustrate the point, let’s match where there are only three items.

.grid {
  ...

  &:has(> :nth-child(3)):not(:has(> :nth-child(4))) {
    > :nth-child(3) {
      grid-column: span 2;
    }
  }
}

So we’re not going to change the 2-column grid, we’ll leave that alone from two. And now we’re not selecting the grid itself, but just grabbing that third item and stretching it across both columns of the grid.

Four

We can… do nothing. It’s already a two-column grid from two. So let’s let it be.

Five+

This is the fun part. We already know how to test for X+ children, so we do that:

.grid {
  ...

  &:has(:nth-child(5)) {
    grid-template-columns: unset;
  }
}

But now we’re unseting those columns, as we don’t need them anymore. Instead we’re going with automatic column creation in the column direction. We could use flexbox here too essentially but we’re already in a grid and grid can do it with easy sturdy columns so might as well. Then we’ll slap smooth scrolling and scroll snapping on there, which will essentially be the fallback behavior (only Chrome supports the ::scroll-button stuff that makes it carousel-like for now).

.grid {
  ...

  &:has(:nth-child(5)) {
    grid-template-columns: unset;

    grid-auto-flow: column;
    grid-auto-columns: 200px;

    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;

    > div {
      scroll-snap-align: center;
    }
  }
}

Actually Carouselling

We’re all set up for it, we just need those back/forward buttons to make it really be a carousel. That’s a CSS thing now, at least in Chrome ‘n’ friends, so we can progressively enhance into it:

.grid {
  ...

  &:has(:nth-child(5)) {
    ...

    anchor-name: --⚓️-carousel;

    &::scroll-button(*) {
      position: absolute;
      top: 0;
      left: 0;
      position-anchor: --⚓️-carousel;
      background: none;
      border: 0;
      padding: 0;
      font-size: 32px;
    }

    &::scroll-button(right) {
      position-area: center inline-end;
      translate: -3rem -0.5rem;
      content: "➡️" / "Next";
    }

    &::scroll-button(left) {
      position-area: inline-start center;
      translate: 3rem -0.5rem;
      content: "⬅️" / "Previous";
    }
  }
}

That’ll do it! Here’s the demo and I’ll video it in case you’re not in Chrome.

]]>
https://frontendmasters.com/blog/quantity-query-carousel/feed/ 1 6323
Container Query for “is there enough space outside this element?” https://frontendmasters.com/blog/container-query-for-is-there-enough-space-outside-this-element/ https://frontendmasters.com/blog/container-query-for-is-there-enough-space-outside-this-element/#comments Tue, 13 May 2025 16:26:46 +0000 https://frontendmasters.com/blog/?p=5796 Say you had a UI component that had pagination arrows for whatever reason. If there was enough space on the outside of that component, you wanted to put those arrows outside, like this this:

But if there isn’t enough room for them without shrinking the main content area, then place them inside, like this:

You could do that with plenty of magic numbers, especially in how big the main content area is. But wouldn’t it be cool if you didn’t have to know? Like the main content area could be whatever, fluid/responsive/flexible, and you could still test if there is “room” outside for the arrows or not.

I was playing with this trick because I remember Adam Argyle talking about it one time, but couldn’t find where he used it. So I wrote this article up to re-learn and document it. Then of course I find the original article. So full credit to Adam here. Mine approach here is super similar of course. I think I prefer how his @container query uses cqi units inside of it in case the parent isn’t the viewport. Clever.

The trick is in combining viewport units within a container query. You could probably do it by using container units within a media query too, but we’ll go with the former because I tried it and it worked.

We’re going to need a “wrapper” element because that’s just how @container queries tend to be most useful. You can’t query the same element that is the container, so easier if the container is a wrapper.

<div class="box">
  <div class="box-inner">
    <div class="arrow arrow-left">
       <svg ...></svg>
    </div>
    <div class="arrow arrow-right">
       <svg ...></svg>
  </div>
</div>

The box will be the container:

.box {
  container: box / inline-size;
  inline-size: min(500px, 100vw);
}

I love using that second declaration, which says: “Make the element 500px wide, but if the entire browser window is smaller than that, do that instead.” That element is the container.

Then we can use a @container query on the inside. If we wanted to make a style change exactly when the container is the same size as the browser window, we could do this:

.box-inner {
  background: rebeccapurple;
  ...

  @container box (inline-size >= 100vw) {
    background: red;
  }
}

That will do this!

But we’re actually dealing with the arrows here, so what we want to know is “is there enough space outside for them?” Meaning not the exact size of the element, but that:

Element <= Viewport - Arrows - Gap

Which we can express like this:

.box-inner {
  background: rebeccapurple;
  ...

  @container box (inline-size <= calc(100vw - 80px * 2 - 1rem * 2)) {
    /* move arrows here */
  }
}

I’ll use a bit of translate to move the arrows here:

And here’s a video of the success:

Again what’s kinda cool about this is that we don’t know what the size of the container is. It could be changed anytime and this will continue to work. The only hard coded numbers we used were for the size of the arrow elements and the gap, which you could abstract out to custom properties if you wanted to be more flexible.

]]>
https://frontendmasters.com/blog/container-query-for-is-there-enough-space-outside-this-element/feed/ 4 5796