Stacking Animations With animation-composition

An icon that floats gently up and down, and also spins when hovered. A notification badge that pulses in scale, and also shakes when a new message arrives. A card that drifts in on a scroll timeline, and also lifts on hover. Each pair is two animations on the same element, and when both touch transform, the second silently replaces the first: the icon stops floating the moment it starts spinning. This page explains why animations on the same property overwrite each other, how the individual transform properties sidestep the problem, and how animation-composition combines animations when they genuinely share a property. It belongs to Keyframe Animation Patterns in the CSS-Only Micro-Interactions & Animations guide.

Why the second animation wins

An element can run several animations at once by listing them: animation: float 3s infinite, spin 600ms. Each animation produces a value for each property in its keyframes. When two animations produce a value for the same property, the browser must choose, and by default it uses the one later in the list — the replace composite operation. The earlier animation keeps running internally, but its effect on that property is hidden.

For transform, that is almost never what a designer intends. float animates transform: translateY(-4px) and spin animates transform: rotate(360deg); while both run, only the rotation shows, and the icon drops back to its un-floated position for the duration of the spin.

Replace versus combine Three columns. Replace: the float and spin animations both set transform, and only the spin is applied. Separate properties: float sets translate and spin sets rotate, and both apply. animation-composition add: both set transform and their transforms are appended, and both apply. float + spin on the same element replace (default) transform: float transform: spin only spin visible float drops out separate properties translate: float rotate: spin both visible no conflict at all composition: add transform: float + transform: spin both visible values combined Try separate properties first; use composition when they must share one.

The first fix: individual transform properties

The simplest solution is often not to share a property at all. CSS has individual translate, rotate and scale properties, each independent of transform and of each other. An animation on translate and an animation on rotate never conflict; the browser applies them in a fixed order — translate, then rotate, then scale, then the transform property — and both effects are visible.

Live demoTwo animations sharing transform versus separate properties
Both icons float. Hover each: the left one drops to its resting height while it spins; the right one keeps floating as it spins.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Stacking animations</title>
<style>
  body { font: 14px/1.5 system-ui, sans-serif; margin: 2rem; display: flex; gap: 3rem; }

  .icon {
    display: grid;
    place-items: center;
    inline-size: 3.5rem;
    block-size: 3.5rem;
    border-radius: 12px;
    background: #6366f1;
    color: #ffffff;
    font-size: 1.5rem;
  }

  /* BROKEN: both animations set transform; on hover the spin replaces the
     float, so the icon drops to its resting height while spinning. */
  .icon--broken {
    animation: float-transform 3s ease-in-out infinite alternate;
  }
  .icon--broken:hover {
    animation: float-transform 3s ease-in-out infinite alternate,
               spin-transform 600ms cubic-bezier(0.22, 1, 0.36, 1);
  }
  @keyframes float-transform { to { transform: translateY(-8px); } }
  @keyframes spin-transform  { to { transform: rotate(360deg); } }

  /* FIXED: each animation owns its own individual property. */
  .icon--fixed {
    animation: float 3s ease-in-out infinite alternate;
  }
  .icon--fixed:hover {
    animation: float 3s ease-in-out infinite alternate,
               spin 600ms cubic-bezier(0.22, 1, 0.36, 1);
  }
  @keyframes float { to { translate: 0 -8px; } }
  @keyframes spin  { to { rotate: 360deg; } }

  @media (prefers-reduced-motion: reduce) {
    .icon, .icon:hover { animation: none; }
  }
</style>
</head>
<body>
  <div class="icon icon--broken" aria-hidden="true">★</div>
  <div class="icon icon--fixed" aria-hidden="true">★</div>
</body>
</html>

Hover each icon. The broken one snaps down to its resting height as the spin begins, because the spin's transform replaces the float's. The fixed one keeps floating while it spins, because translate and rotate are separate properties with separate animation values.

One caveat applies to the hover pattern itself: re-declaring animation in the hover state restarts every animation in the list, including the float. The restart is barely noticeable here because the float is gentle, but for more prominent loops, keep the loop on a wrapper element and the hover effect on the child, so the loop is never re-declared.

The key technique: animation-composition for shared properties

Sometimes separate properties are not enough. Two effects may both need scale — a continuous breathing pulse and a one-off pop on click — or both need filter, or the element's own transform must be preserved while an animation adds to it. animation-composition tells the browser to combine an animation's value with the underlying value instead of replacing it.

replace, add and accumulate on scale An element with an underlying scale of 1.1 runs an animation to scale 1.2. replace gives 1.2. add appends scale transforms, giving 1.1 times 1.2, about 1.32. accumulate adds the scale deltas, giving about 1.3. Underlying scale 1.1, animation to 1.2 replace 1.2 underlying ignored add ≈ 1.32 1.1 × 1.2, applied in turn accumulate ≈ 1.3 deltas summed: 1 + 0.1 + 0.2 For translate and rotate, add and accumulate give the same visible result.
  • replace (default) — the animation's value replaces the underlying value.
  • add — the animation's value is added to the underlying value. For transforms, the lists are concatenated, so both are applied in sequence.
  • accumulate — the values are combined numerically. Scale factors combine by adding their differences from 1, lengths and angles simply sum.

The underlying value is whatever the property would be without this animation: the element's own style, plus any animations earlier in the list. That makes composition a way to layer effects in a controlled order.

.badge {
  scale: 1;
  animation:
    breathe 2.4s ease-in-out infinite alternate,
    pop 300ms cubic-bezier(0.34, 1.56, 0.64, 1) var(--pop-delay, 0s) both;
  /* The second animation adds to whatever the first produces. */
  animation-composition: replace, add;
}

@keyframes breathe { to { scale: 1.06; } }
@keyframes pop     { 50% { scale: 1.25; } }

@media (prefers-reduced-motion: reduce) {
  .badge { animation: none; }
}

animation-composition is a list like the other animation longhands, paired by index with animation-name. Here the breathing loop replaces the element's resting scale as usual, and the pop adds on top of it, so the pop starts from wherever the breathing currently is rather than snapping to a fixed scale.

Order of application

When several transform sources apply to one element — the individual properties, the transform property, and several animations with different composite modes — the result depends on a fixed order, and it helps to know it when effects combine unexpectedly.

The individual properties are always applied in the same sequence: translate first, then rotate, then scale, and finally whatever transform contains. Because transforms compose right to left in their effect on the element's coordinate space, that sequence means scaling happens around the element's own origin before it is rotated and moved, which is almost always what designers expect. It is why a card can scale up on hover with scale while an unrelated animation moves it with translate, without the move distance being scaled.

Within a single property, animations are layered in the order they appear in the animation-name list, each composing onto the result of those before it according to its own animation-composition value. A replace animation later in the list discards everything below it for that property; an add animation builds on it. Putting the "base" loop first and the momentary effects after it, with add, gives the intuitive result that momentary effects happen on top of the loop.

Transitions sit below animations in this stack. A transitioning translate provides the underlying value for any animation on translate, so an add animation can bob an element while a transition slides it to a new position — both visible.

Composition beyond transforms

Composition applies to any animatable property, which enables layering that would otherwise need extra wrapper elements:

  • Filters. A continuous hue shift and a one-off brightness flash both on filter can coexist with add, since filter lists concatenate.
  • Colours and lengths. With accumulate, an animation can nudge a length relative to the element's current value — "8px more than whatever the layout gave it" — rather than to a fixed number.
  • Scroll-driven plus time-based. A scroll-linked parallax on translate and a hover lift on the same property can combine with add, so hovering a card mid-scroll lifts it from its scrolled position. Scroll-Driven Parallax Effects uses this combination.

Composition does not replace good structure. When effects belong to conceptually different things — a card's position in a list and a hover affordance — separate elements or separate properties remain clearer. Composition is for effects that truly modify the same visual quality.

Browser support

The individual translate, rotate and scale properties are supported in Chrome and Edge 104+, Firefox 72+ and Safari 14.1+. animation-composition is supported in current versions of Chrome, Edge, Firefox and Safari; in an engine without it, the declaration is ignored and animations replace each other as before, so layered effects degrade to the later animation only. Running multiple animations on one element via a comma-separated list has been supported since CSS animations were introduced: Chrome 43, Edge 12, Firefox 16, Safari 9.

FAQ

What happens when two animations target the same property? By default the later animation in the animation-name list replaces the earlier one's value for that property, so only one effect is visible. animation-composition: add or accumulate changes that, combining the values instead.

What is the difference between add and accumulate?add appends values where the property allows lists, so two transforms become one longer transform list applied in sequence. accumulate combines values numerically, adding lengths, angles and scale factors together. For most transforms both look similar; for filters and scale they can differ.

Can I avoid animation-composition by using separate properties? Often yes. The individual transform properties translate, rotate and scale each animate independently, so a bobbing translate animation and a spinning rotate animation never conflict. Reach for animation-composition when both effects genuinely need the same property.

Does animation-composition affect performance? No meaningful difference. Composited properties such as transform and opacity remain on the compositor whether their values are replaced or combined.

Related articles

More pages in the same section.