Animations can make or break your website’s user experience. But, if you’re using a third-party library to create animations, it’s important to choose carefully. The same library that enables you to create complex animations will often have a significant impact on your website’s overall performance. Before making any final decisions, be sure to weigh the pros and cons of adding animations to your site.
Most libraries offer techniques for improving website animations, but they are often optional. One approach for creating fast and performant animations is to use a JavaScript promise to create asynchronous animations that don’t block the main thread on the browser.
Shifty is a JavaScript animation library that offers first-class support for this feature. In this article, we’ll explore Shifty’s features and demonstrate how it utilizes JavaScript promises to create high-performing animations that can be used with async/await functions.
Jump ahead:
To follow along with the examples in this article, you should have a fundamental understanding of JavaScript and promises.
async/await animations refer to the use of JavaScript asynchronous functions and the await
keyword to create smooth and performant animations. This approach allows for the execution of animations without blocking the main thread on the browser.
JavaScript is synchronous by default, so code execution occurs in sequence. In other words, the JavaScript code will wait for the animation to start before loading other site functionalities. However, with asynchronous animations, the website can load without waiting for the animation.
This approach involves creating an async
function containing the animation logic and using the await
keyword to wait for the completion of each animation step before moving on to the next.
Here’s how the structure of an async/await animation looks:
(async () => { const { ... } = await tween({...}); const { ... } = await tween({...}); tweenable.tween({...}); })();
In this example, when the Immediately Invoked Function Expression (IIFE) function gets invoked, it waits for the first animation command (or tween
) to execute before moving to the next. By utilizing this approach, developers can create intricate and sophisticated animations that are both readable and easy to maintain.
There are several advantages to using the async/await approach for creating animations, including:
Shifty is a JavaScript tweening library for building fast, asynchronous, and efficient animations for web applications. Tweening libraries manage the transition and translation of an object’s property value, resulting in the desired motion of the animated object.
This low-level animation library simplifies the process of animating HTML elements using JavaScript — without the need for complex CSS or third-party plugins. It focuses on optimal performance and flexibility, making it ideal for developers who want to create customizable and extensible animation tools.
Shifty’s unique selling proposition lies in its inbuilt support for executing animations using JavaScript promises. However, the library offers many additional benefits that are worth highlighting:
You can add Shifty to your project using a package manager, such as npm or Yarn, or via CDN.
If you have a simple vanilla JavaScript application that doesn’t utilize a package manager, you can simply integrate Shifty into the project by adding either of these links within a <script>
tag in your markup: https://unpkg.com/shifty or https://unpkg.com/shifty/dist/shifty.core.js.
Alternatively, you can use npm or Yarn to add it as a dependency in your project using the following commands:
npm install --save shifty //or yarn add shifty
N.B., you’ll need to have Node.js installed on your machine to add Shifty as a dependency
Shifty is comprised of two primary classes, namely the Tweenable
and Scene
classes. These classes export methods that let us create and control how animations are rendered.
Tweenable
classThe Tweenable
class is the core building block of Shifty. It provides the main API used to define an animation’s start and end state. It also lets us define custom easing functions that control the acceleration and declaration of animation, as well as the interpolation between different states of an object over a specified period of time. It does this by breaking down the animation into smaller steps or “tweens” that move the object from one state to another.
Here’s the simplest way to use the Tweenable
class:
import { tween } from "shifty"; tween({ from: { x: 0, y: 50 }, to: { x: 10, y: -30 }, duration: 1500, easing: "easeOutQuad", render: (state) => console.log(state), }).then(() => console.log("All done!"));
We can instantiate a tweenable
instance that lets us reuse tweens and have more control over the animation:
const { tween } = shifty; const element = document.querySelector("#tweenable"); const animate = async () => { const { tweenable } = await tween({ render: ({ x }) => { element.style.transform = `translateX(${x}px)`; }, easing: "easeInOutQuad", duration: 300, from: { x: 0 }, to: { x: 200 }, }); await tweenable.tween({ to: { x: 0 }, }); await tweenable.tween({ to: { x: 200 }, }); }; animate();
Shifty’s tweens are functions that accept an object argument with properties that define the target element to be animated, the start and end point, duration, and easing type for an animation.
These properties are as follows:
from
: defines the starting values of the tween animationto
: defines the ending values of the tween animationduration
: specifies the length of time, in milliseconds, that the tween animation should take to completeeasing
: specifies the easing function that should be used to control the rate of change of the tween over timerender
: specifies the function that should be called at each step of the tween animation; this function is responsible for updating the target element’s style properties to reflect the current state of the tween animationEach tween
in Shifty is a promise; therefore, we can chain a .then()
callback that will execute after the tween
has been completed:
const element = document.querySelector("#tweenable"); tween({ render: ({ x }) => { element.style.transform = `translateX(${x}px)`; }, easing: "easeInOutQuad", duration: 500, from: { x: 0 }, to: { x: 300 }, }) .then(({ tweenable }) => tweenable.tween({ to: { x: 200 }, }) ) .then(({ tweenable }) => tweenable.tween({ to: { x: 100 }, }) ) .then(({ tweenable }) => tweenable.tween({ to: { x: 0 }, }) );
Alternatively, we can use JavaScript’s async/await syntax to avoid chaining .then()
callbacks, resulting in a more concise and readable code structure:
const element = document.querySelector("#tweenable"); (async () => { const { tweenable } = tween({ render: ({ x }) => { element.style.transform = `translateX(${x}px)`; }, easing: "easeInOutQuad", duration: 500, from: { x: 0 }, to: { x: 300 }, }); await tweenable.tween({ to: { x: 200 }, }); await tweenable.tween({ to: { x: 100 }, }); await tweenable.tween({ to: { x: 0 }, }); })();
With this approach, we can generate multiple autonomous tweens that adhere to the promise flow and are executed sequentially.
async/await’s capability to run within loops without blocking the thread enables us to generate infinite animation loops without getting stuck in an endless cycle:
const element = document.querySelector("#tweenable"); (async () => { while (true) { const { tweenable } = tween({ render: ({ x }) => { element.style.transform = `translateX(${x}px)`; }, easing: "easeInOutQuad", duration: 500, from: { x: 0 }, to: { x: 300 }, }); await tweenable.tween({ to: { x: 200 }, }); await tweenable.tween({ to: { x: 100 }, }); await tweenable.tween({ to: { x: 0 }, }); } })();
Thanks to the async/await block, the while
loop functions exactly as intended here, even though in a synchronous function, it would lead to an infinite loop and cause the page to crash.
We can also utilize the try/catch block to create more intricate animations. The concept behind this technique involves creating a tween within an IIFE async function with a try/catch block wrapped in a while
loop. When the animation is rejected, the animation’s promise is treated as a caught exception, and the control flow is diverted to the catch
block, which is then utilized to execute another animation function:
const { style } = document.querySelector("#tweenable"); const tweenable = new shifty.Tweenable( { scale: 1, color: "#ee6b33", x: 100, y: 100, }, { render: ({ scale, color }) => { style.transform = `scale(${scale})`; style.background = color; }, } ); const tween = tweenable.tween.bind(tweenable); const switcher = async () => { while (true) { try { await tween({ to: { scale: 0.5, color: "#333" }, easing: "bouncePast", duration: 750, }); await tween({ delay: 200, to: { scale: 1, color: "#db3d9f" }, easing: "easeOutQuad", duration: 750, }); break; } catch (e) {} } }; (async () => { while (true) { try { await tween({ delay: 200, to: { scale: 0.75, color: "#eebc33", }, duration: 1200, easing: "easeOutQuad", }); await tween({ delay: 100, to: { scale: 1, color: "#68db3d", }, }); } catch (e) { await switcher(); } } })(); document.addEventListener("click", (e) => { tweenable.cancel(); });
In this example, the animation gets rejected as soon as you click anywhere in the browser, thus causing the catch
block to await the switcher
function. The switcher
function is another async
function that leverages a similar pattern to change the cube’s color
.
Once the switcher
function breaks and exits its while
loop, the initial IIFE function repeats. Unlike the try/catch block within the IIFE function, the switcher catch
block doesn’t have any operation; hence, the while
loop simply restarts and tries again:
See the Pen
Shifty Example by David Omotayo (@david4473)
on CodePen.
Scene
classShifty’s Scene
class provides a mechanism that lets us group and coordinate related tweenable
objects to create complex animations, with the ability to play, pause, stop, and seek the entire scene as a whole.
A set of methods called members manages the timing and sequence control. Here are some of the primary methods of the Scene
class:
scene.play()
: starts playing the scene, executing all tweenable
objects in the scene:See the Pen
Shifty scene.play() example by David Omotayo (@david4473)
on CodePen.
scene.pause()
: stops the scene from playing, halting the execution of any tweenable
objects currently running:See the Pen
Shifty scene.pause() example by David Omotayo (@david4473)
on CodePen.
scene.stop()
: stops the scene from playing, likewise halting the execution of any tweenable
objects currently running:See the Pen
Shifty scene.stop() example by David Omotayo (@david4473)
on CodePen.
scene.resume()
: resumes the execution of paused scenes:See the Pen
Shifty scene.resume() example by David Omotayo (@david4473)
on CodePen.
These members can easily create animations with multiple moving parts that need to be synchronized and coordinated in time:
See the Pen
Shifty scene demo by David Omotayo (@david4473)
on CodePen.
In this article, we demonstrated how Shifty uses asynchronous JavaScript code to produce fast, high-performance animations. We examined Shifty’s fundamental components and looked at various techniques for constructing animations using the Tweenable
and Scene
classes.
The illustrations presented in this article may not be exceedingly intricate, but they demonstrate how we can utilize Shifty and async/await functions to generate dynamic and engaging animations with minimal JavaScript code.
There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.
LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.
LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Build confidently — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSimplify component interaction and dynamic theming in Vue 3 with defineExpose and for better control and flexibility.
Explore how to integrate TypeScript into a Node.js and Express application, leveraging ts-node, nodemon, and TypeScript path aliases.
es-toolkit is a lightweight, efficient JavaScript utility library, ideal as a modern Lodash alternative for smaller bundles.
The use cases for the ResizeObserver API may not be immediately obvious, so let’s take a look at a few practical examples.