Home / Articles / Animation & Motion /
Animation composition: accumulate and add
Advanced animation control: how multiple keyframe animations combine using add and accumulate composition modes.
the animation-composition property
When multiple animations target the same CSS property, the browser must decide how to combine them. The animation-composition property controls this behavior with three modes: replace (default), add, and accumulate.
@keyframes move {
from { transform: translateX(0); }
to { transform: translateX(100px); }
}
.element {
animation: move 2s linear infinite;
animation-composition: add;
}
This is an advanced feature primarily used for creating complex combined animations that would be difficult or impossible with a single @keyframes block.
replace: the default behavior
replace is the default composition mode. When multiple animations target the same property, the most specific (or last-applied) animation wins. Other animations are essentially ignored.
/* Only the last animation for each property wins */
.box {
/* Animation 1 */
animation: rotate 2s linear infinite;
}
/* Later in cascade or via class...
.box.active {
animation: move 2s linear infinite;
/* move replaces rotate because same property (transform) */
}
This default behavior is why you often see animations defined together: transform: rotate(...) translateX(...) in a single keyframe block rather than split across multiple animations.
add: adding values together
The add mode adds animated values together. For numeric properties like transform or opacity, this means the values sum up each frame.
/* Animation A: rotates to 360deg */
@keyframes rotate {
to { transform: rotate(360deg); }
}
/* Animation B: translates to 100px */
@keyframes translate {
to { transform: translateX(100px); }
}
/* Combined with add: element rotates AND translates */
.composite {
/* Note: this is conceptual - actual syntax requires
Web Animations API or multiple animations on same element */
animation: rotate 2s linear forwards, translate 2s linear forwards;
/* Each frame: current rotation + current translation */
}
accumulate: building up across iterations
The accumulate mode is unique: it applies the animation's effect cumulatively across iterations. Each loop adds to the previous result rather than resetting.
/* Spinning accumulates — never resets to 0deg */
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.cumulative-spin {
animation: spin 1s linear infinite;
animation-composition: accumulate;
/* Iteration 1: 0° → 360°
Iteration 2: 360° → 720°
Iteration 3: 720° → 1080°
... and so on */
}
This eliminates the "reset" feeling of infinite animations. Instead of bouncing back and forth with alternate, the animation continues forward indefinitely.
when to use each mode
Use add when layering effects: combining rotation with translation, or scaling with color change. Each animation handles one aspect while add merges them smoothly.
accumulate for: continuous rotation, counters or progress indicators, animations that build complexity over time. Use add when you need true compositional blending of multiple animation streams.browser support considerations
animation-composition has limited support in 2026. It works in Chrome 116+, Edge 116+, and Safari 17+. Firefox support is partial. Always test or provide a fallback.
.element {
/* Fallback: single combined keyframe */
animation: combined 2s linear infinite;
}
@keyframes combined {
to {
transform: rotate(360deg) translateX(100px);
/* Pre-composed since add may not be supported */
}
}
/* Progressive enhancement for supporting browsers */
@supports (animation-composition: add) {
.element {
animation: rotate 2s linear infinite, translate 2s linear infinite;
animation-composition: add, add;
}
}
Deep dive: replace versus add versus accumulate
Understanding the precise mechanics of each composition mode is essential for predicting animation behavior. The replace mode operates on a cascade principle where the animation appearing later in the style cascade completely replaces earlier animations targeting the same property. This is not a blend or merge—it's a hard switch. When you have two animations both setting transform, the browser picks one entirely, discarding the other.
The add mode performs mathematical addition of animated values each frame. For transform properties with numeric values, this means each transform function's argument is summed independently. If one animation rotates to 90deg and another translates to 50px, with add the result is both transforms applied simultaneously: the element rotates while moving. Critically, add operates on the delta—the change from each animation's current frame—not the absolute values. This distinction becomes important when animations have different durations or easing curves.
accumulate operates differently again: it only affects multi-iteration animations. Instead of resetting to the initial keyframe value after each iteration, the final computed value becomes the starting point for the next loop. This creates a true cumulative effect where transformations compound. A rotation animation that reaches 360deg in its first iteration will begin its second iteration at 360deg, progressing to 720deg, then 1080deg, and so on. Only transform and color properties meaningfully support accumulation, as other properties lack a mathematical sense of "adding" to a final state.
Practical patterns: stacking state-driven animations
One of the most valuable real-world applications of animation composition is handling intersecting interactive states. Consider an element that should respond to both focus and hover: focus might apply a glow effect while hover applies a subtle scale. Without composition, you'd need combinatorial keyframes like :hover:focus, :hover:not(:focus), etc. With add, each state gets its own animation and they blend naturally.
/* Base glow on focus */
@keyframes glow {
from { box-shadow: 0 0 0 oklch(0.5 0.2 250); }
to { box-shadow: 0 0 20px oklch(0.6 0.3 250); }
}
/* Scale on hover */
@keyframes subtle-scale {
from { transform: scale(1); }
to { transform: scale(1.05); }
}
.button:focus {
animation: glow 1.5s ease-in-out infinite alternate;
animation-composition: add;
}
.button:hover {
animation: subtle-scale 0.3s ease forwards;
animation-composition: add;
}
/* Both effects combine: glowing AND scaled */
This pattern scales beyond simple states. You can layer a loading pulse with a hover effect, combine a shake animation triggered by form validation with a pulsing error border, or stack a parallax scroll effect on top of a fade-in entry animation. Each animation concerns itself with one dimension of behavior while the composition mode handles merging them.
Layered scroll-triggered animations
Scroll-driven animations are becoming increasingly common with the animation-timeline property. Composition modes enable sophisticated layered effects where different animation properties respond to scroll position independently. You might animate an element's translate on scroll while simultaneously rotating it over a different scroll range.
/* Parallax movement over full scroll */
@keyframes parallax {
from { transform: translateY(0); }
to { transform: translateY(-100px); }
}
/* Rotation over a specific section's scroll range */
@keyframes section-rotate {
from { transform: rotate(0deg); }
to { transform: rotate(180deg); }
}
.parallax-element {
animation: parallax linear;
animation-timeline: scroll(root);
animation-composition: add;
}
.parallax-element.active-section {
animation-name: section-rotate;
animation-timeline: view();
animation-range: entry 20% cover 80%;
animation-composition: add;
}
/* Element translates AND rotates based on different scroll triggers */
The beauty of this approach is separation of concerns. The parallax logic doesn't need to know about the section rotation, and vice versa. This modular approach makes complex scroll narratives easier to maintain and modify. You can add a third animation layer—say, a opacity fade—in the future without rewriting existing keyframes.
Interaction with @keyframes and transform lists
Animation composition interacts in subtle ways with how keyframes are structured. Recall that transform functions compose through multiplication, not addition: rotate(90deg) translateX(100px) is fundamentally different from translateX(100px) rotate(90deg). The order in your keyframe string determines the final geometry.
When you use the add composition mode with transform animations, the browser adds each transform function's numerical arguments independently rather than concatenating the transform list strings. This means a rotation animation and a translation animation composing with add will effectively create a dynamic equivalent of placing both functions in a single keyframe block, with the browser handling the intermediate state calculations.
However, this also means composition has limitations. You cannot add transform functions of different types in a meaningful way—the browser doesn't add rotation to scale the way it adds translation to translation. For mixed transform types, you generally want the transforms in a single @keyframes definition where order is explicit. Composition excels when layering the same transform type (two rotations, two translations) or when separating concerns across independently triggered animations.
Browser support and progressive enhancement
As of 2026, animation-composition has decent but incomplete support. Chrome and edge (chromium 116+) fully support all three modes via replace, add, and accumulate. Safari added support in version 17, though early implementations had quirks around accumulation with CSS colors. Firefox has been slower to adopt; as of Firefox 125, support remains partial with add working inconsistently for composite properties.
/* Feature detection approach */
@supports (animation-composition: add) {
.composite-element {
animation: spin 2s linear infinite, scale 2s ease infinite;
animation-composition: add, add;
}
}
@supports not (animation-composition: add) {
.composite-element {
/* Fallback: pre-compose in keyframes */
animation: spin-and-scale 2s linear infinite;
}
}
@keyframes spin-and-scale {
to {
transform: rotate(360deg) scale(1.1);
background-color: oklch(0.7 0.2 60);
}
}
The progressive enhancement strategy here is straightforward: provide a pre-composed single animation as the baseline, then layer the composed version for supporting browsers. This ensures the animation works everywhere while giving composing browsers richer, more modular behavior.
Critical animations—those required for functionality or accessibility—should always be designed with composition fallbacks in mind. Decorative animations can use a feature-detection pattern with a @supports block, but interactive feedback like focus states or error animations should function even when composition isn't available. In cases where composition fails gracefully, the animation may be less nuanced but it should still provide the necessary feedback.
const el = document.querySelector('.composite');
const rotate = el.animate(
{ transform: ['rotate(0deg)', 'rotate(360deg)'] },
{ duration: 2000, iterations: Infinity }
);
const translate = el.animate(
{ transform: ['translateX(0px)', 'translateX(100px)'] },
{ duration: 2000, iterations: Infinity }
);
/* Browsers combine transforms automatically via compositor */
In the WAAPI, the browser applies transforms from multiple animations via the compositor. This often makes add unnecessary for transform properties.
Cascade timing and composition order
When multiple animations are defined with different composition modes, the order in which they're applied matters. The browser builds up the final computed style through a specific sequence: first, replace mode animations establish baseline values, then add animations layer on top, and accumulate effects persist across iterations. Within a single animation shorthand that lists multiple keyframe names, the order of those names dictates which animation's values are added to which.
This ordering becomes critical when dealing with animations that have mismatched durations. If animation A runs for 2 seconds and animation B runs for 4 seconds, their add composition means that after 2 seconds, A's values reset to starting while B continues. The effect may look jarring unless you account for this timing discrepancy, often by looping the shorter animation as well or by making both animations multiples of a base duration.
The animation-direction property also interacts with composition in unexpected ways. An alternate animation composing with a normal animation can produce counterintuitive results: as one reverses and the other continues forward, the composition flips between adding and subtracting values. This can create a pulsing effect where the combined value expands then contracts, which may be desirable for certain UI feedback patterns or may require careful tuning to avoid unintended visual strobing.
Performance considerations and the compositor
Animation composition introduces performance tradeoffs that developers should understand. When animations can be entirely composited—meaning they only affect properties handled by the GPU compositor like transform and opacity—the composition happens on the compositor thread, parallel to the main thread's rendering work. This is the performance sweet spot where add composition has essentially no overhead.
However, when composing animations that affect layout or paint properties, the main thread must recompute the combined value each frame. This adds computational cost, and in extreme cases with many stacked animations, can contribute to jank. A good rule of thumb: if your composed animation dips below 60 frames per second, try reducing the number of composition layers or converting properties to compositable equivalents. For example, instead of composing on width and height, compose on transform: scale().
/* Preferred: composes on compositor-friendly properties */
@keyframes fade-move {
from { opacity: 0.5; transform: translateY(0); }
to { opacity: 1; transform: translateY(-20px); }
}
@keyframes pulse-scale {
from { transform: scale(1); }
to { transform: scale(1.03); }
}
.composite {
/* Both animations affect opacity or transform — compositor handles */
animation: fade-move 1s ease, pulse-scale 0.5s ease infinite alternate;
animation-composition: add;
}
/* Avoid: layout-triggering properties in composition */
@keyframes bad-composite {
to { width: 200px; height: 100px; }
}
Modern browsers are getting better at optimizing composition, but the fundamental cost of adding animated values each frame remains. For complex compositions involving multiple animations with different durations, durations that share common factors (like 1s, 2s, and 4s) are more performant than prime durations (1.3s, 1.7s, 2.9s) because they synchronize more predictably, reducing the frequency of recalculation.
Summary
Animation composition is a powerful tool for building complex, maintainable animation systems. Here are the key takeaways:
replaceis default — later animations override earlier ones in the cascade.addsums animated values each frame, enabling layered visual effects.accumulatecarries effects across iterations, eliminating reset points in infinite animations.- Composition shines for state-driven overlays: hover+focus, scroll-triggered layers, validation animations.
- Transform order in keyframes matters:
rotate() translate()differs fromtranslate() rotate(). - Support is growing but not universal — always provide pre-composed fallbacks.
- Prefer composited properties (
transform,opacity) for best performance. - For simple cases, one combined
@keyframesblock is often clearer than composition.