Custom CSS animations in React
Dynamically generated CSS3 animations in React
The problem
While CSS3 offers a declarative way to define animations on elements, it is not enough sometimes. Although it supports custom timing with a highly configurable quadratic Bézier curve, it does not support arbitrary functions. The general case is a Javascript function that given the timing returns the calculated state would control the animation.
Sadly it can not be achieved in a purely declarative way unlike the simpler functions. Fortunately there are multiple solutions, ones that can be done with the React framework.
Solutions
At least two different approaches exist. The first one uses some kind of a timer to periodically update the animated element (most likely using requestAnimationFrame). This is a less hacky and precise solution. The main drawback is that the calculate function has to be called for every frame as long as the animation is running; it makes it infeasible in situations where the calculation is a resource intensive task.
The other train of thought is to calculate the animation beforehand, then generate CSS3 animation keyframes. This is a more hacky solution, but has the advantage that the path has to be calculated only once per change. The drawback is that it is not precise, as there are only so many keyframes. This is a performance - precision trade-off, however it generally yields good results. The users are not likely to spot the misplaced frames if you interpolate linearly between 100 calculated points.
In this post I'll demonstrate the latter technique with a simple code.
Code
Code is available here.
This demonstration centers on an animated element accepting functions for both X and Y translations. These two timing functions map the 0 - 1 interval to the same 0 - 1 interval; they control how much translations should be applied for a given juncture.
Live Demo
Generate the style tag
The first thing is to generate the style tag and handle it's removal when the component itself is removed. We also need to salt the class names with a random value, in order to support multiple instances.
getInitialState() {
const random = Math.round(Math.random() * 10000000);
Generating the contents is the next step. We now need to decide on the precision, as the number of keyframes is specified here.
...
const style = document.createElement("style");
const keyframesNums = 100;
const css = `@keyframes animation_${random} {
${_.map(_.range(0, keyframesNums), (index, idx, list) => { // This generates 100 keyframes (0 ... 99%)
return `${Math.round(index / (list.length - 1) * 100)}% {}`;
}).join("\n")}
}`;
The animation will be less precise if you use fewer keyframes. It can reach to a point where it is becoming increasingly noticeable. For example the above animation will look more of a diamond than a circle when you use only ten keyframes:
Lastly, we need to insert and store the tag, along with the random nonce.
...
style.type = "text/css";
style.appendChild(document.createTextNode(css));
document.getElementsByTagName("head")[0].appendChild(style);
return {
random,
style
};
},
Using the animation specified above requires a simple style attribute:
<div
className="AnimatedElement"
style={ {animation: `5s animation_${this.state.random} infinite linear`} }
>
It is important that you set the interpolation to linear, as it defaults to ease. Failing to override it would seriously mess up the animation.
We also need to take care of the cleanup:
componentWillUnmount() {
this.state.style.remove();
},
Update the style
We need to tap into the lifecycle at the componentDidMount and the componentDidUpdate events.
componentDidMount() {
this.updateAnimation();
},
componentDidUpdate() {
this.updateAnimation();
},
At the updateAnimation method we can iterate on and update the keyframes based on the current results of the animation functions.
updateAnimation() {
_.each(this.state.style.sheet.cssRules[0].cssRules, (cssRule, index, list) => {
const position = index / (list.length - 1);
const animationPositionX = this.props.animationFunctionX(position);
const animationPositionY = this.props.animationFunctionY(position);
cssRule.style.transform = `translate(${animationPositionX * 100}px, ${animationPositionY * 100}px)`;
});
},
Note, that modifying the keyframes this way won't change the DOM, so it will be invisible in the source. It makes debugging a bit harder.
Performance considerations
The main drawback of this solution is that the keyframes are updated for every update of the component. This can be mitigated somewhat by adding a comparison to the componentDidUpdate callback, to check for referential equality. But for most cases, this won't help much as the function is constructed dynamically, making a different instance every time.
Functions are next to impossible to compare due to the closures they have access to, but you can add a hash to them. This should be affected by all the parameters the function uses, and must be kept in sync.
const func = (timing) => {
return ...;
}
func.hash = ...;
if (prevProps.func.hash !== this.props.func.hash) {
this.updateAnimation();
}