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:

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.


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.