Skip to content

mkbabb/keyframes.js

Repository files navigation

keyframes.js image

CSS keyframe animations for anything in JavaScript. Specify your keyframes in standards-compliant CSS; animate any object, DOM element, or data structure.

demo

Quick Start

Create a CSSKeyframesAnimation, feed it CSS @keyframes, add targets, play:

const anim = new CSSKeyframesAnimation({
    duration: 2000,
    iterationCount: Infinity,
    direction: "alternate",
    fillMode: "forwards",
});

anim.fromString(`
    @keyframes mijn-keyframes {
        from {
            transform: translateX(-100%) translateY(-100%) rotate(0turn);
            background-color: #C462D8;
        }
        100% {
            transform: translateX(50%) translateY(75%) rotate(1turn);
            background-color: #E85252;
        }
    }
`);

anim.setTargets(document.getElementById("myElement"));
anim.play();

This animates the element's style properties as specified in the keyframes. The default behavior, but you can get far more inventive with it.

Plucked directly from the demo/simple Vue file.

Table of Contents

Installation

npm install @mkbabb/keyframes.js

Works both in and out of the browser. Anything that touches the DOM (getComputedStyle, document, etc.) won't work in Node.

Project Structure

src/
├── animation/           # Animation engine: classes, group, presets, interpolation, WAAPI
│   ├── index.ts         # Animation, CSSKeyframesAnimation
│   ├── group.ts         # AnimationGroup — multi-animation compositor
│   ├── animations.ts    # 30+ preset animations
│   ├── constants.ts     # Types & defaults
│   ├── utils.ts         # Frame calc, value interpolation
│   └── waapi.ts         # Web Animations API delegation
├── parsing/             # CSS @keyframes parsing & serialization
│   ├── keyframes.ts     # @keyframes grammar (parse-that combinators)
│   ├── format.ts        # Animation → CSS string (Prettier)
│   ├── units.ts         # Re-export: CSS value/color parsers
│   └── utils.ts         # Re-export: parser combinators
├── units/               # Value types & normalization
│   ├── normalize.ts     # DOM-aware unit normalization
│   ├── color/           # Color space classes, converters, normalization
│   └── *.ts             # Re-export barrels from @mkbabb/value.js
├── easing.ts            # Re-export: easing functions
├── math.ts              # Re-export: clamp, lerp, bezier
└── utils.ts             # Re-export + memoizeDecorator

demo/                    # Vue 3 demo apps
├── @/                   # Shared: animation controls, shadcn-vue UI, styles
├── cube/                # 3D sphere + AnimationGroup + Three.js (default demo)
├── simple/              # Minimal single-animation example
├── square/              # Custom transform function
├── amiga/               # Physics-like 3D sphere
├── balls/               # Vanilla JS: CSS vars + staggered animations
├── boxes/               # Vanilla JS: matrix3d transforms
└── bench/               # Performance benchmarks (rAF vs CSS vs WAAPI)

test/                    # Vitest (jsdom) — 11 suites
bench/                   # Vitest benchmarks — 3 suites

Animation

The Animation object drives CSSKeyframesAnimation and AnimationGroup. It's composed of:

  • options (AnimationOptions)
  • transform function: interpolates between keyframes
  • timing function: eases the animation
  • keyframes (TemplateAnimationFrame)

AnimationOptions

  • duration: time in milliseconds (default: 1000)
  • delay: time in milliseconds before the animation begins (default: 0)
  • iterationCount: number of repetitions (default: 1)
  • direction: normal, reverse, alternate, alternate-reverse
  • fillMode: none, forwards, backwards, both
  • timingFunction: easing function (default: easeInOutCubic)
  • colorSpace: color interpolation space (default: oklab)
  • useWAAPI: delegate to Web Animations API when eligible (default: true)

The transform function

type TransformFunction<V extends Vars> = (v: V, t: number) => void;

Called at each timestep t (0 to duration) with the interpolated variables v. The variables arrive in the same shape you specified in your keyframes—deeply nested objects included.

Every value is parsed as a CSS value unit (1px, 1em, 1deg, etc.). To interpolate between two units they must share a supertype: px and em are both length, so they interpolate; px and deg are not.

The timing function

type TimingFunction = (t: number) => number;

Takes t in [0, 1], returns [0, 1]. All CSS timing functions are implemented in easing.ts.

Step functions

Implemented as steppedEase(t, steps, jumpTerm):

  • jump-none: the value is held until the end of the step
  • jump-start / start: step occurs at the start
  • jump-end / end: step occurs at the end
  • jump-both / both: steps at both boundaries

Bezier curves

cubicBezier(t, x1, y1, x2, y2) implements the cubic case. The general case uses de Casteljau's algorithm iteratively.

Both are in math.ts. For Bezier visualizations, see this Desmos graph or the timing-functions demo.

CSSCubicBezier is the higher-order convenience: takes control points, returns a t → value function. CSS's named easings are built from it:

const easeInBounce = (t: number) => CSSCubicBezier(0.09, 0.91, 0.5, 1.5)(t);

TemplateAnimationFrame

A template keyframe prior to resolution. Composed of:

  • id: auto-incremented identifier
  • start: start time (CSS time string, number, or percentage)
  • vars: the variables to interpolate
  • transform: per-keyframe transform function (optional)
  • timingFunction: per-keyframe timing function (optional)

Once a transform or timing function is specified, it propagates to all subsequent keyframes.

Reification

Template keyframes are reified into concrete keyframes by:

  1. Parsing start times: CSS time formats (1s, 100ms), raw numbers, or percentages (50%). All normalized to a percentage of total duration.
  2. Resolving functions: null transforms and timing functions fall back to the AnimationOptions defaults.
  3. Sorting by start percentage.
  4. Resolving variables across keyframes—every keyframe ends up with the same variable set.
  5. Computing durations from sorted start/stop times.

Variable resolution

Keyframes needn't declare the same variables. The resolver walks backward from each keyframe, seeking the most recent definition of each variable. If none exists, the default value (typically 0) is used.

const kf1 = { start: "0s",   vars: { x: 0, y: 0 } };
const kf2 = { start: "500ms", vars: { x: 1 } };
const kf3 = { start: "100%", vars: { x: 0, y: 1 } };

kf2 gets y: 0 from kf1. This lets you specify keyframes loosely.

CSSKeyframesAnimation

An abstraction over Animation that parses CSS @keyframes into TemplateAnimationFrame objects, then adds them to a base Animation.

Three ways to create keyframes:

// From CSS string (memoized parse)
anim.fromString(`from { opacity: 0; } to { opacity: 1; }`);

// From keyframe map
anim.fromKeyframes({ "0%": { opacity: 0 }, "100%": { opacity: 1 } });

// From variable array
anim.fromVars([{ opacity: 0 }, { opacity: 1 }]);

The default transform applies interpolated values to element.style for each target.

Parsing CSS keyframes

keyframes.ts covers most of the CSS spec:

  • from, to, and percentages
  • Time units (s, ms)
  • Lengths (px, em, etc.)
  • Angles (deg, rad, etc.)
  • Colors (#fff, rgb(255, 255, 255), lab(100, 0, 0), lightblue, etc.)
  • Transforms (translateX(100%), rotate(1turn), etc.)
  • Variables (var(--my-var))
  • Math expressions (calc(100% - 10px))
  • Any key: value pair parseable as a CSS value, function, or list thereof

The parser uses @mkbabb/parse-that and @mkbabb/value.js for CSS value parsing. All exported parse functions are memoized.

Units

Thorough unit parsing and resolution, covering:

  • length, angle, time, resolution, percentage, color

See units/ and parsing/units.ts.

A unit value takes one of three forms:

  • ValueUnit: a value with a string unit and an array of supertypes
  • FunctionValue: a function name with an array of ValueUnits
  • ValueArray: an array of ValueUnits

Each defines toString(), valueOf(), and lerp(t, other, target?). Any ValueUnit variant can interpolate with any other—values are aligned to the shorter array, then interpolated element-wise.

Units of the same supertype interpolate freely. Supertypes also encode whether a unit is relative or absolute (px is absolute; em is relative), used to resolve to a common base before interpolation.

Interpolation dispatch: numeric values use lerp; colors use perceptual interpolation (oklab by default, configurable); computed units (vh, vw, calc, var) resolve against the live DOM at interpolation time.

AnimationGroup

Composites multiple animations with layer blending. Each animation gets a layer config controlling how it merges with others.

const group = anim1.group(anim2, anim3);
group.setTargets(element);
group.play();

Three blend modes:

  • replace: highest zIndex wins (default)
  • add: numeric values accumulate
  • weighted: linear interpolation by weight (0–1)

Layer configuration per animation: zIndex, weight, blendMode, enabled, properties (whitelist). Property whitelisting enables effect layering—one layer animates position, another animates opacity.

The group manages its own requestAnimationFrame loop and marks child animations as managed.

Presets

30+ ready-to-use animations in animations.ts. All return CSSKeyframesAnimation instances and accept optional InputAnimationOptions:

import { fadeIn, bounce, spinner } from "@mkbabb/keyframes.js";

const anim = fadeIn({ duration: 500 });
anim.setTargets(element);
anim.play();

Fade: fadeIn, fadeOut · Attention: pulse, heartbeat, glow, shake, bounce · Entrance/Exit: flip, rotateIn, slideIn, slideInLeft/Right, slideOutLeft/Right, blurIn/Out/InOut, jumpUp/Down, warpLeft/Right · Effects: hover, typewriter, typingCursor, rainbowText, progressBar, skeletonLoading, spinner, parallaxScroll, gradientBackground, rotateScale, accordionExpand, notificationBounce, textFocusBlur

Web Animations API

When useWAAPI is true (default), eligible animations run on the compositor thread via Element.animate(). Eligibility requires: DOM targets, uniform timing function across frames, no computed units, no custom transform function, no LAB/OKLAB colors. Falls back to requestAnimationFrame silently.

Build & Development

npm run build        # library → dist/keyframes.js + keyframes.cjs + keyframes.d.ts
npm run gh-pages     # demo → dist/
npm run dev          # vite dev server on :8080 (cube demo)
npm test             # vitest (jsdom)
npm run bench        # vitest bench

Dependencies: @mkbabb/value.js (ValueUnit, Color, math, parsing, normalization) and @mkbabb/parse-that (parser combinators).

TypeScript: strict: true, verbatimModuleSyntax: true, target: ES2022, moduleResolution: bundler.

Node: >=22. License: GPL-3.0.

Sources, acknowledgements, &c.

Releases

No releases published

Packages

 
 
 

Contributors