How to use a Custom Easing Function with the Web Animations API (WAAPI)

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 of `0.5` also is `0.5`. For `translate` you then end up with `200% * 0.5` = `100%` when midway the animation.
• For the bounce function, the output for an input of `0.5` is `0.765625`. Do the multiplication with the delta and you end up with a translation of `200% * 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 `lerp()`, 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.

~

To help spread the contents of this post, feel free to retweet the announcements made on social media:

~

🔥 Like what you see? Want to stay in the loop? Here's how: