Stacked Transforms

Chris Coyier Chris Coyier on

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);
}Code language: CSS (css)

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)
  }
}Code language: CSS (css)

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);
}Code language: CSS (css)

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)
  }
}Code language: CSS (css)

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); */
  }
}
Code language: CSS (css)

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) */
  }
}
Code language: CSS (css)

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;
  }
}
Code language: CSS (css)

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.

Wanna learn CSS Animations deeply?

Leave a Reply

Your email address will not be published. Required fields are marked *

Did you know?

Frontend Masters Donates to open source projects. $363,806 contributed to date.