So, you’ve found a custom easing function defined in JavaScript. Great! But how do you use that with the Web Animations API (WAAPI)? Turns out that’s more difficult than you’d first expect it to be.
~
# Bounce it!
Take this custom bounce easing as found on easings.net:
// Bounce easing function
const bounce = function (pos) {
const n1 = 7.5625;
const d1 = 2.75;
if (pos < 1 / d1) {
return n1 * pos * pos;
} else if (pos < 2 / d1) {
return n1 * (pos -= 1.5 / d1) * pos + 0.75;
} else if (pos < 2.5 / d1) {
return n1 * (pos -= 2.25 / d1) * pos + 0.9375;
} else {
return n1 * (pos -= 2.625 / d1) * pos + 0.984375;
}
};
Unlike what you might think, simply passing the function into the easing
option of a WAAPI-backed animation won’t work:
// Animate element via JS
const anim = document
.querySelector('.bounce')
.animate([
{ translate: "0% 0%" },
{ translate: "0% 100%" },
], {
duration: 2000,
easing: bounce, // ❌ Won’t work!
fill: "both",
iterations: Infinity
});
To use it, you need to jump through some hoops …
~
# It’s all about input and output
An easing function is nothing but a simple function takes an input value and produces an output value. The input value typically falls within the [0, 1]
range.
// Calculate output for the given input
const easingInput = 0.5; // This value typically ranges between 0 and 1
const easingOutput = easingFn(easingInput);
For a linear easing, the output is the same as the input.
// Linear easing function
const linear = (input) => {
const output = input;
return output;
}
Use the tool below to see how this works. Simply drag the slider to change the input. The output value and the chart will update in response.
The tool also sports a chart that visualizes the easing function by plotting the input on the x
axis and the output on the y
axis.
See the Pen Easing Function Input Output (Linear Easing) by Bramus (@bramus) on CodePen.
For a bounce easing, the formula (mentioned at the start of this post) is a bit more complicated, but the premise remains the same: it’s a function that produces a certain output value for a given input.
See the Pen Easing Function Input Output (Bounce Easing) by Bramus (@bramus) on CodePen.
~
# Method 1: Using precalculated keyframes
Consider these keyframes for an animation that I want to use. It consists of only a start and end keyframe:
[
{ translate: "0% 0%" },
{ translate: "0% 200%" },
]
When used, these keyframes translate an element on the Y-axis from 0%
to 200%
.
Calculating a single keyframe
The specific value for translate
at a certain point in the animation depends on the easing function that’s used. For a regular linear easing when mid-animation, you can quite easily guess it: the element will be positioned halfway in between 0%
and 200%
on the Y-axis, thus as 100%
. For a custom easing function, that’s a bit harder to guess.
But you don’t really need to guess anything. The formula to compute a specific keyframe value is a simple multiplication: multiply the easing function’s output by the delta between the start value and end value, and you’ve got the value to use in the keyframe.
For the keyframes described earlier, the delta value for the translate
property is 200%
. This is the result of subtracting the start value (0%
) from the target value (200%
).
With all those pieces available, you can now compute the keyframes to use at any point in the animation. For example, to know the translate
position halfway the animation‘s duration:
- For a regular
linear
easing, the output for an input of0.5
also is0.5
. Fortranslate
you then end up with200% * 0.5
=100%
when midway the animation. - For the bounce function, the output for an input of
0.5
is0.765625
. Do the multiplication with the delta and you end up with a translation of200% * 0.765625
=153.125%
when midway the animation.
Put into code, you’d end up with this:
// Calculate output for the given input
const easingInput = 0.5; // This value typically ranges between 0 and 1
const easingOutput = easingFn(easingInput);
// Calculate the delta
const startValue = 0;
const targetValue = 200;
const deltaValue = targetValue - startValue; // Here, effectively 200 again
// Compute the keyframe
const keyframe_for_input = {
translate: `0% ${startValue + (deltaValue * easingOutput)}%`,
};
Expanding the visualization tool, you get this:
See the Pen Easing Function Input Output (Bounce Easing) + Animation by Bramus (@bramus) on CodePen.
Calculating all keyframes
Repeat the calculation for a single keyframe using a bunch of values within the [0, 1]
range, say 100 times, and you can calculate all the necessary keyframes that make up an animation effect.
Those keyframes can then be used with the Web Animations API as you’d normally use them. Since the keyframes themselves are already following the custom easing – the values are precalculated – you can use a regular linear
easing combined with it.
In code, that becomes this:
// Helper to precalculate a bunch of keyframes for a given easing function
const buildKeyframes = (easing, keyframe, points = 50) => {
const result = [...new Array(points + 1)]
.map((d, i) => easing(i * (1 / points)))
.map((value) => keyframe(value))
;
return result;
};
// Calculate the delta
const startValue = 0;
const targetValue = 200;
const deltaValue = targetValue - startValue; // Here, effectively 200 again
// Build 100 keyframes using custom easing function
const keyframes = buildKeyframes(
bounce,
(v) => ({
translate: `0% ${startValue + (deltaValue * v)}%`,
}),
100
);
// Animate element via JS
const anim = document
.querySelector('.bounce[data-method="js"]')
.animate(
keyframes, // 👈 use the precalculated keyframes
{
duration: 2000,
easing: "linear", // 👈 since the keyframes are precalculated, use a regular linear easing.
fill: "both",
iterations: Infinity
}
);
The code uses a helper function buildKeyframes
to do the heavy lifting. It takes three arguments:
easing
- The custom easing function that you want to use.
keyframe
- A function that builds a single keyframe for a given output value.
points
- How many keyframes you want to calculate. The higher the number the smoother the animation. Note that this can come with a performance cost.
The following demo is built with it:
See the Pen Custom Easing Function with the Web Animations API (WAAPI) by Bramus (@bramus) on CodePen.
~
# Method 2: Using linear()
A recent addition to the web platform is the linear()
easing function. It allows you to reproduce complex easing functions in a simple manner. This is done by taking the original curve, and simplifying it to a series of points. In between those points – and this explains the name of the function – a linear interpolation happens.
An example of a curve represented through linear()
is this:
linear(0, 0.06, 0.25 18%, 1 36%, 0.81, 0.75, 0.81, 1, 0.94, 1, 1);
The cool thing is, is that you can use linear()
as a value for a WAAPI’s easing
option by passing it in as a string.
Converting a custom easing function to linear()
is done in similar fashion as the conversion done in method 1: run various input values ranging from [0, 1]
through the function and note the output value. The resulting output values can be used directly in linear()
here, since linear()
also uses values in the [0, 1]
range.
To easily do this conversion, Michelle Barker shared a handy function to do this:
// Convert a custom easing to linear()
// Source: https://developer.mozilla.org/en-US/blog/custom-easing-in-css-with-linear/#recreating_popular_eases_with_javascript
const toLinear = (easing, points = 50) => {
const result = [...new Array(points + 1)]
.map((d, i) => easing(i * (1 / points)));
return `linear(${result.join(",")})`;
};
const anim_linear = document
.querySelector('.bounce[data-method="linear"]')
.animate([
{ translate: '0% 0%' },
{ translate: '0% 200%' },
], {
duration: 2000,
easing: toLinear(bounce, 100), // 👈 Create a `linear(…)` variant of bounce with 100 points.
fill: "both",
iterations: Infinity
});
This is effectively the same approach as method 1. Key difference here though is that you are precalculating the curve instead of the keyframes themselves.
A more advanced tool to generate linear()
functions the linear()
generator by Jake Archibald. It also does SVG and has some finer control options.
The visual result is also the same, as demonstrated in this demo:
See the Pen Custom Easing Function with the Web Animations API (WAAPI) by Bramus (@bramus) on CodePen.
Compatibility-wise there’s more to say here as linear()
only became Baseline in 2023. Support for linear()
landed in Chrome 113, Firefox 112, and Safari 17.2 – that means older versions of those browsers will fall back to a regular linear
easing as they don’t understand linear()
.
Note the difference between linear
and linear()
. The former is a regular linear
easing in which the output is exactly the same as the input. The latter is the function that allows you to simplify a complex curve to a set of points.
In hindsight, we maybe should have called the latter one something else, to prevent confusion with linear
. Naming things is hard.
~
# Method 3: Use a CustomEffect
Part of the Web Animations 2 specification are Custom Effects. These allow you to define effects through script. This essentially is a function that gets called for every frame that gets passed in the animation’s progress which ranges from 0
to 1
.
Its intended use is to do something like syncing a video to an animation, but nothing is stopping you from doing the translation in there.
const $subject = document.querySelector('.bounce[data-method="custom-effect"]');
const animation = new Animation();
animation.effect = new CustomEffect((progress) => {
$subject.style.transform = `translateY(${CSS.percent(200 * bounce(progress))}`;
}, 2000);
animation.play();
Custom Effects are not supported by any stable browser at the time of writing. Safari has an experimental implementation behind a feature flag. The spec also needs work, so this feature might change a lot in the future.
Custom Effects are not as performant as regular animations, as they always run on the main thread.
~
# Method 4: Register the easing function
A proposal within the CSS Working Group is to allow developers to register their custom easing functions. The proposal suggests a new method document.registerTimingFunction
to do that.
document.registerTimingFunction('bounce', function (pos) {
const n1 = 7.5625;
const d1 = 2.75;
if (pos < 1 / d1) {
return n1 * pos * pos;
} else if (pos < 2 / d1) {
return n1 * (pos -= 1.5 / d1) * pos + 0.75;
} else if (pos < 2.5 / d1) {
return n1 * (pos -= 2.25 / d1) * pos + 0.9375;
} else {
return n1 * (pos -= 2.625 / d1) * pos + 0.984375;
}
});
Once registered, the method would become available to use with WAAPI and CSS animations.
.animated {
animation-timing-function: bounce;
}
This is still just a proposal. Nothing formal about it right now, yet I’d love to see this one move forward.
~
# Combined demo
Here’s both approaches that work in today’s browsers (i.e. methods 1 and 2) combined in one demo:
See the Pen Custom Easing Function with the Web Animations API (WAAPI) by Bramus (@bramus) on CodePen.
~
# Spread the word
To help spread the contents of this post, feel free to retweet the announcements made on social media:
New post: “How to use a Custom Easing Function with the Web Animations API (WAAPI)”
So, you’ve got a custom easing function. Great! But how do you use that with the Web Animations API? Turns out it’s more difficult than you’d first expect it to be …
🔗 https://t.co/aVb8rGCOLB pic.twitter.com/5ziBWy7y1V
— Bramus (@bramus) January 12, 2024
~
🔥 Like what you see? Want to stay in the loop? Here's how: