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.

Animation of a bounce easing curve (in blue) getting simplified by plotting some dots on it. In between the dots, a linear interpolation happens. The result is a curve (in yellow) that resembles the original one.

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.

~

# Spread the word

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

~

Published by Bramus!

Bramus is a frontend web developer from Belgium, working as a Chrome Developer Relations Engineer at Google. From the moment he discovered view-source at the age of 14 (way back in 1997), he fell in love with the web and has been tinkering with it ever since (more …)

Unless noted otherwise, the contents of this post are licensed under the Creative Commons Attribution 4.0 License and code samples are licensed under the MIT License

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.