Additive Animations in CSS

The following is supported in the latest Firefox, Edge, and Chrome (and also Safari Technology Preview). Yay.

What happens when you have two simultaneous animations in CSS that modify the same property?

div {
  animation:
    x 2000ms infinite alternate ease-in,
    y 2000ms infinite alternate ease-out;
}
@keyframes x {
  from { transform: translateX(0vw) }
  to { transform: translateX(90vw) }
}
@keyframes y {
  from { transform: translateY(0vh) }
  to { transform: translateY(90vh) }
}

View Demo

Only one transform value can be set at a given time, so only one of the translations will occur (the last one in the animation list wins, so for this example it is the vertical animation with translateY()).

The Web Animations API allows us to mix up this behavior through the introduction of the composite option.

Additive Animation

First, we can make an equivalent animation in the WAAPI as the previous CSS example:

element.animate({
  transform: ['translateX(0vw)','translateX(90vw)']
}, {
  duration: 2000,
  easing: 'ease-in',
  iterations: Infinity,
  direction: 'alternate'
});

element.animate({
  transform: ['translateY(0vh)','translateY(90vh)']
}, {
  duration: 2000,
  easing: 'ease-in',
  iterations: Infinity,
  direction: 'alternate'
});

The default behavior remains the same, so this code will do the exact same as the CSS, where only the vertical translation will occur.

It does let us start playing with a new option, however, called the composite property. Its default value is 'replace' which is the behavior we have discussed so far, where a value on a property is replaced by a newer animation that comes along and tries to modify the same property.

If we change the second animation to add a composite: 'add' option, we tell it to add the value of this animation to whatever the current state of that property is.

element.animate({
  transform: ['translateY(0vh)','translateY(90vh)']
}, {
  duration: 2000,
  easing: 'ease-in',
  iterations: Infinity,
  direction: 'alternate',
  composite: 'add'
});

Now, instead of replacing the value defined by the first animation, it adds our new value. For transform which has a syntax that accepts a list of transform functions, adding means appending the new transform functions to the end of the existing list. The filter property is another list-based property that will allow you to keep appending new values to the list of functions.

See the Pen Transform with composite add by Dan Wilson (@danwilson) on CodePen.

For properties that are not lists, add behaves slightly differently, but in line with what you may expect. Properties that accept any number-based valu (such as lengths, percentages, or raw numbers) will add the numbers together. So if you have two animations affecting opacity, the resulting visual opacity will be the sum of the current values of opacity in each animation.

See the Pen Opacity Animations with `composite: 'add'` by Dan Wilson (@danwilson) on CodePen.

The same goes for moving an item 20px + 30px with margin left (not the most performant way to move an object, but it demonstrates length usage)… if the animations both run at the same time, with the same duration and in the same direction, the end result will be a movement of 50px.

Bringing it to CSS

Currently, there is no way to specify this same behavior in CSS, though there have been discussions to add it. That doesn’t mean our CSS animations cannot take advantage of this option today.

With the getAnimations() method on elements (including the document) you can fetch all the WAAPI animations, CSS animations, and CSS transitions that are active on the element. More importantly, we can modify the keyframes, timing options, or composite property on any animation returned. So all of a sudden our animations (even our CSS ones) can be updated at any time (even while they are running).

All that to say… you can use the magic of composite on CSS animations and transitions by adding a little bit of JavaScript:

const animated = document.getElementById('my-element')
animated.getAnimations().forEach(animation => {
  animation.effect.composite = 'add';
});

See the Pen Additive CSS Animations by Dan Wilson (@danwilson) on CodePen.

Once you have an element, you can call getAnimations() on it, iterate through the array of animations, and update each one to update the composite property. If you need to only add it to certain animations in the list, you can check the animation.animationName property for CSS Animations or use other methods to determine which animation is which.

This can also go along with the animationstart and other events to make sure you are updating the property at an appropriate time. These events do not contain an Animation object in the handler, but they do return identifying information such as the animationName. So we can cross reference this event.animationName with the animationName found in the getAnimations() list for CSS Animations to pinpoint and update any animation we are looking for.

animated.addEventListener('animationstart', e => {
  const all = animated.getAnimations();
  const myAnimation = all.find(anim => anim.animationName === e.animationName);
  myAnimation.effect.composite = 'add';
});