WAAPI in Practice: Building a Lightweight Animation Library

The Web Animations API (WAAPI) gives JavaScript direct access to the browser’s animation engine—the same machinery that powers CSS animations and transitions. Because the browser handles the heavy lifting internally, WAAPI animations avoid the jank and hacky workarounds that often accompany window.requestAnimationFrame()-based approaches.

One of the biggest wins of moving animations from stylesheets to JavaScript is separation of concerns. Instead of toggling CSS classes and scoping properties onto elements to control playback, you can manage an animation entirely in code. That makes it possible to dynamically change durations, keyframes, and property values on the fly—something that's awkward with pure declarative CSS.

That flexibility is exactly what @okikio/animate leverages. It's a WAAPI wrapper inspired by animateplus and animejs, tuned for performance and developer experience. The minified and gzipped bundle comes in at roughly 5.79 KB.

From PJAX to an Animation Library

The library grew out of a broader project. In 2020, the author set out to build a more efficient PJAX library—similar to Rezo Zero's Starting Blocks but with the ease of use of barbajs. PJAX, for those unfamiliar, enables smooth page-to-page transitions by fetching content and swapping DOM elements rather than doing full page reloads.

After surveying sites that used PJAX poorly—often breaking scrollbars, over-prefetching, and ignoring users with slower connections—the approach shifted toward progressive enhancement. That effort became the "native initiative" (stored in the okikio/native monorepo), aiming to deliver modern features in a performant, standards-compliant, and lightweight way.

While testing the PJAX library on a real project, the author discovered that no existing animation libraries were built on WAAPI, so he created @okikio/animate. It started as a simple wrapper and has since reached about 80% feature parity with more mature animation libraries. (If you're using React and just want quick animate.css-style effects, use-web-animations is worth a look—it was developed around the same time.)

How the Library Wraps WAAPI

Creating animations with @okikio/animate means instantiating an Animate class, which wraps a set of targets. Behind the scenes, it builds a list of WAAPI Animation instances—one per target—plus a "main animation" that tracks overall progress by animating a non-visible element. This extra layer exists to smooth out inconsistencies between browser vendor implementations of WAAPI.

The main animation is stored in Animate.prototype.mainAnimation, and per-target animation instances live in a WeakMap keyed by each target's KeyframeEffect. To fetch the animation object for a specific element, you use Animate.prototype.getAnimation(el).

You don't need to internalize every architectural detail to use the library, but the design shows how the WAAPI's open-ended structure lends itself to custom layers on top.

Since constructing an Animate instance manually is verbose, the library exposes a convenience animate function that handles instantiation for you:

import animate from "@okikio/animate";
// or
import { animate } from "@okikio/animate";

animate({ 
  target: [/* ... */],
  duration: 2000,
  // ... 
});

A basic animation looks like this:

import animate from "@okikio/animate";

// Do this if you installed it via the script tag: const { animate } = window.animate;

(async () => {
  let [options] = await animate({
    target: ".div",

    // Units are added automatically for transform CSS properties
    translateX: [0, 300],
    duration: 2000, // In milliseconds
    speed: 2,
  });

  console.log("The Animation is done...");
})();

Developer Experience Features

Apart from abstracting the raw WAAPI, @okikio/animate includes several conveniences. It accepts both target and targets keywords for specifying what to animate, merging both lists and eliminating duplicates using Set. Animation option values can be functions—not just static values—which enables patterns like animejs-style staggering. (Note that the function argument order differs from animejs; the README's section on "Animation Options & CSS Properties as Methods" explains the details.)

Current Limitations

WAAPI is a living standard, and @okikio/animate evolves along with it. There are still some gaps in its feature set.

No Built-in Timeline

The library deliberately omits a formal timeline feature. The reasoning:

  • Async/await programming support makes explicit timelines less necessary, and the timelineOffset animation option covers animejs-style timeline needs.
  • Keeping the package small was a priority.
  • Group and sequence effects are coming to the Web Animations API spec; it made sense not to grow the API surface until real-world usage demands it.

Custom Easing Support

Custom easings—springs, elastic, and similar—aren't yet built in. The author recommends following Jake Archibald's easing worklet proposal as the most promising path forward. In the meantime, a spring animation approach inspired by Kirill Vasiltsov's WAAPI article is planned.

Unit and Color Handling

Automatic units are supported for transform functions like translateX, translate, scale, and skew, but there are still restrictions on which CSS color properties work. The v2.2.0 release notes detail those specifics. For example:

animate({
  targets: [".div", document.querySelectorAll(".el")],

  // By default "px", will be applied
  translateX: 300,
  left: 500,
  margin: "56 70 8em 70%",

  // "deg" will be applied to rotate instead of px
  rotate: 120, 

  // No units will be auto applied
  color: "rgb(25, 25, 25)",
  "text-shadow": "25px 5px 15px rgb(25, 25, 25)"
});

What's Next for WAAPI and the Library

Future WAAPI features are on the horizon. ScrollTimeline support is likely on the way—it's already in Chrome Canary 92—which would enable scroll-linked animations natively. The library's timeline option was built with this in mind:

(This demo may require Chrome Canary with the Experimental Web Platform features flag enabled; it may behave fine on Firefox.)

Another goal is shrinking the bundle further from its ~5.79 KB size. To check that figure yourself, this article recommends bundle.js.org, which bundles code locally in your browser; bundlephobia has issues with this particular package.

Polyfilling for Older Browsers

To support WAAPI in older environments, you'll need the web-animations-next.min.js polyfill—especially for timeline support and the KeyframeEffect constructor. The typical setup tests for KeyframeEffect support before loading the polyfill, and you should avoid adding async or defer attributes to it. You'll also want polyfills for Map, Set, and Promise:

<html>
  <head>
    <!-- Async -->
    <script src=",es2015,es2018,Array.prototype.includes,Map,Set,Promise" async></script>
    <!-- NO Async/Defer -->
    <script src="./js/webanimation-polyfill.min.js"></script>
  </head>
  <body>
    <!-- Content -->
  </body>
</html>

For ES6+ builds, esbuild handles transpiling, bundling, and minifying in one pass. For ES5 output, combining esbuild (without minification) with TypeScript and terser is faster and more reliable than a full Babel setup—the project's Gulpfile demonstrates this workflow.