Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Wed, 17 Sep 2025 13:49:39 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.3 225069128 CSS offset and animation-composition for Rotating Menus https://frontendmasters.com/blog/css-offset-and-animation-composition-for-rotating-menus/ https://frontendmasters.com/blog/css-offset-and-animation-composition-for-rotating-menus/#respond Wed, 17 Sep 2025 13:49:38 +0000 https://frontendmasters.com/blog/?p=7147 Circular menu design exists as a space-saver or choice, and there’s an easy and efficient way to create and animate it in CSS using offset and animation-composition. Here are some examples (click the button in the center of the choices):

I’ll take you through the second example to cover the basics.

The Layout

Just some semantic HTML here. Since we’re offering a menu of options, a <menu> seems appropriate (yes, <li> is correct as a child!) and each button is focusable.

<main>
  <div class="menu-wrapper">
    <menu>
      <li><button>Poland</button></li>
      <li><button>Brazil</button></li>
      <li><button>Qatar</button></li>
      <!-- etc. -->
    </menu>
    <button class="menu-button" onclick="revolve()">See More</button>
  </div>
</main>

Other important bits:

The menu and the menu button (<button id="menu-button">) are the same size and shape and stacked on top of each other.

Half of the menu is hidden via overflow: clip; and the menu wrapper being pulled upwards.

main { 
  overflow: clip;
}
.menu-wrapper { 
  display: grid;
  place-items: center;
  transform: translateY(-129px);
  menu, .menu-button {
    width: 259px;
    height: 129px;
    grid-area: 1 / 1;
    border-radius: 50%;
  }
}

Set the menu items (<li>s) around the <menu>’s center using offset.

menu {
    padding: 30px;
    --gap: 10%; /* The in-between gap for the 10 items */
}
li {
  offset: padding-box 0deg;
  offset-distance: calc((sibling-index() - 1) * var(--gap)); 
  /* or 
    &:nth-of-type(2) { offset-distance: calc(1 * var(--gap)); }
    &:nth-of-type(3) { offset-distance: calc(2 * var(--gap)); }
    etc...
  */
}

The offset (a longhand property) positions all the <li> elements along the <menu>’s padding-box that has been set as the offset path.

The offset CSS shorthand property sets all the properties required for animating an element along a defined path. The offset properties together help to define an offset transform, a transform that aligns a point in an element (offset-anchor) to an offset position (offset-position) on a path (offset-path) at various points along the path (offset-distance) and optionally rotates the element (offset-rotate) to follow the direction of the path. — MDN Web Docs

The offset-distance is set to spread the menu items along the path based on the given gap between them (--gap: 10%).

ItemsInitial value of offset-distance
10%
210%
320%

The Animation

@keyframes rev1 { 
  to {
    offset-distance: 50%;
  } 
}

@keyframes rev2 { 
  from {
    offset-distance: 50%;
  } 
  to {
    offset-distance: 0%;
  } 
}

Set two @keyframes animations to move the menu items halfway to the left, clockwise, (rev1), and then from that position back to the right (rev2)

li {
  /* ... */
  animation: 1s forwards;
  animation-composition: add; 
}

Set animation-time (1s) and animation-direction (forwards), and animation-composition (add) for the <li> elements

Even though animations can be triggered in CSS — for example, within a :checked state — since we’re using a <button>, the names of the animations will be set in the <button>’s click handler to trigger the animations.

By using animation-composition, the animations are made to add, not replace by default, the offset-distance values inside the @keyframes rulesets to the initial offset-distance values of each of the <li>.

ItemsInitial Valueto
10%(0% + 50%) 50%
210%(10% + 50%) 60%
320%(20% + 50%) 70%
rev1 animation w/ animation-composition: add
Itemsfromback to Initial Value
1(0% + 50%) 50%(0% + 0%) 0%
2(10% + 50%) 60%(10% + 0%) 10%
3(20% + 50%) 70%(20% + 0%) 20%
rev2 animation w/ animation-composition: add

Here’s how it would’ve been without animation-composition: add:

ItemsInitial Valueto
10%50%
210%50%
320%50%

The animation-composition CSS property specifies the composite operation to use when multiple animations affect the same property simultaneously.

MDN Web Docs

The Trigger

const LI = document.querySelectorAll('li');
let flag = true;
function revolve() {
  LI.forEach(li => li.style.animationName = flag ? "rev1" : "rev2");
  flag = !flag;
}

In the menu button’s click handler, revolve(), set the <li> elements’ animationName to rev1 and rev2, alternatively.

Assigning the animation name triggers the corresponding keyframes animation each time the <button> is clicked.

Using the method covered in this post, it’s possible to control how much along a revolution the elements are to move (demo one), and which direction. You can also experiment with different offset path shapes. You can declare (@keyframes) and trigger (:checked, :hover, etc.) the animations in CSS, or using JavaScript’s Web Animations API that includes the animation composition property.

]]>
https://frontendmasters.com/blog/css-offset-and-animation-composition-for-rotating-menus/feed/ 0 7147
Stacked Transforms https://frontendmasters.com/blog/stacked-transforms/ https://frontendmasters.com/blog/stacked-transforms/#comments Tue, 15 Jul 2025 23:38:20 +0000 https://frontendmasters.com/blog/?p=6457 I think the best way for me to show you what I want to show you is to make this blog post a bit like a story. So I’m gonna do that.


So I’m at CSS Day in Amsterdam this past month, and there was a lovely side event called CSS Café. I’m 90% sure it was during a talk by Johannes Odland and a coworker of his at NRK (whose name I embarrassingly cannot remember) where they showed off something like an illustration of a buoy floating in the water with waves in front of it. Somehow, someway, the CSS property animation-composition was involved, and I was like what the heck is that? I took notes during the presentation, and my notes simply said “animation-composition”, which wasn’t exactly helpful.

I nearly forgot about it when I read Josh Comeau’s blog post Partial Keyframes, where he talks about “dynamic, composable CSS keyframes”, which, as I recall was similar to what Johannes was talking about. There is some interesting stuff in Josh’s post — I liked the stuff about comma-separating multiple animations — but alas, nothing about animation-composition.

So I figured I’d stream about it, and so I did that, where I literally read the animation-composition docs on MDN and played with things. I found their basic/weird demo intriguing and learned from that. Say you’ve got a thing and it’s got some transfoms already on it:

.thing {
  transform: translateX(50px) rotate(20deg);
}

Then you put a @keyframes animation on it also:

.thing {
  transform: translateX(50px) rotate(20deg);
  animation: doAnimation 5s infinite alternate;
}

@keyframes doAnimation {
  from {
    transform: translateX(0)
  }
  to {
    transform: translateX(100px)
  }
}

Pop quiz: what is the translateX() value going to be at the beginning of that animation?

It’s not a trick question. If you intuition tells you that it’s going to be translateX(0), you’re right. The “new” transform in the @keyframes is going to “wipe out” any existing transform on that element and replace it with what is described in the @keyframes animation.

That’s because the default behavior is animation-composition: replace;. It’s a perfectly fine default and likely what you’re used to doing.

But there are other possible values for animation-composition that behave differently, and we’ll look at those in a second. But first, the fact that transform can take a “space-separated” list of values is already kind of interesting. When you do transform: translateX(50px) rotate(20deg);, both of those values are going to apply. That’s also relatively intuitive once you know it’s possible.

What is less intuitive but very interesting is that you can keep going with more space-separated values, even repeating ones that are already there. And there I definitely learned something! Say we tack on another translateX() value onto it:

.thing {
  transform: translateX(50px) rotate(20deg) translateX(50px);
}

My brain goes: oh, it’s probably basically the same as translateX(100px) rotate(20deg);. But that’s not true. The transforms apply one at a time, and in order. So what actually happens is:

Illustration depicting three rectangles with arrows indicating movement and rotation, labeled with numbers 1, 2, and 3, on a dotted background.

I’m starting to get this in my head, so I streamed again the next day and put it to work.

What popped into my head was a computer language called Logo that I played with as a kid in elementary school. Just look at the main image from the Wikipedia page. And the homepage of the manual is very nostoligic for me.

Cover of the LEGO TC logo book titled 'Teaching the Turtle,' featuring a blue and red LEGO robotic structure on a baseplate with a computer in the background.

We can totally make a “turtle” move like that.

All I did here is put a couple of buttons on the page that append more transform values to this turtle element. And sure enough, it moves around just like the turtle of my childhood.

But Mr. Turtle there doesn’t really have anything to do with animation-composition, which was the origin of this whole story. But it’s sets up understanding what happens with animation-composition. Remember this setup?

.thing {
  transform: translateX(50px) rotate(20deg);
  animation: doAnimation 5s infinite alternate;
}

@keyframes doAnimation {
  from {
    transform: translateX(0)
  }
  to {
    transform: translateX(100px)
  }
}

The big question is: what happens to the transform that is already on the element when the @keyframes run?

If we add animation-composition: add; it adds what is going on in the @keyframes to what is already there, by appending to the end of the list, as it were.

.thing {
  transform: translateX(50px) rotate(20deg);
  animation: doAnimation 5s infinite alternate;
  animation-composition: add;
}

@keyframes doAnimation {
  from {
    transform: translateX(0);
    /* starts as if: 
       transform: translateX(50px) rotate(20deg) translateX(0); */
  }
  to {
    transform: translateX(100px);
    /* ends as if:
      transform: translateX(50px) rotate(20deg) translateX(100px); */
  }
}

If we did animation-composition: accumulate; it’s slightly different behavior. Rather than appending to the list of space-separated values, it increments the values if it finds a match.

.thing {
  transform: translateX(50px) rotate(20deg);
  animation: doAnimation 5s infinite alternate;
  animation-composition: accumulate;
}

@keyframes doAnimation {
  from {
    transform: translateX(0);
    /* starts as if: 
       transform: translateX(50px) rotate(20deg); */
  }
  to {
    transform: translateX(100px);
    /* ends as if:
      transform: translateX(150px) rotate(20deg) */
  }
}

It’s not just transform that behave this way, I just found it a useful way to grok it. (Which is also why I had space-separated filter on the mind.) For instance, if a @keyframes was adjusting opacity and we used add or accumulate, it would only ever increase an opacity value.

.thing {
  opacity: .5;
  
  transform: translateX(50px) rotate(20deg);
  animation: doAnimation 2s infinite alternate;
  animation-composition: add;
}

@keyframes doAnimation {
  from {
    opacity: 0;
    /* thing would never actually be 0 opacity, it would start at 0.5 and go up */
  }
  to {
    opacity: 1;
  }
}

So that’s that! Understanding how “stacked” transforms works is very interesting to me and I have a feeling will come in useful someday. And I feel the same way about animation-composition. You won’t need it until you need it.

]]>
https://frontendmasters.com/blog/stacked-transforms/feed/ 2 6457