CSS keyframe animations for anything in JavaScript. Specify your keyframes in standards-compliant CSS; animate any object, DOM element, or data structure.
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.
- Installation
- Project Structure
- Animation
- CSSKeyframesAnimation
- AnimationGroup
- Presets
- Build & Development
npm install @mkbabb/keyframes.jsWorks both in and out of the browser. Anything that touches the DOM (getComputedStyle, document, etc.) won't work in Node.
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
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)
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-reversefillMode:none,forwards,backwards,bothtimingFunction: easing function (default:easeInOutCubic)colorSpace: color interpolation space (default:oklab)useWAAPI: delegate to Web Animations API when eligible (default:true)
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.
type TimingFunction = (t: number) => number;Takes t in [0, 1], returns [0, 1]. All CSS timing functions are implemented in easing.ts.
Implemented as steppedEase(t, steps, jumpTerm):
jump-none: the value is held until the end of the stepjump-start/start: step occurs at the startjump-end/end: step occurs at the endjump-both/both: steps at both boundaries
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);A template keyframe prior to resolution. Composed of:
id: auto-incremented identifierstart: start time (CSS time string, number, or percentage)vars: the variables to interpolatetransform: 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.
Template keyframes are reified into concrete keyframes by:
- Parsing start times: CSS time formats (
1s,100ms), raw numbers, or percentages (50%). All normalized to a percentage of total duration. - Resolving functions: null transforms and timing functions fall back to the
AnimationOptionsdefaults. - Sorting by start percentage.
- Resolving variables across keyframes—every keyframe ends up with the same variable set.
- Computing durations from sorted start/stop times.
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.
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.
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: valuepair 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.
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 supertypesFunctionValue: a function name with an array ofValueUnitsValueArray: an array ofValueUnits
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.
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: highestzIndexwins (default)add: numeric values accumulateweighted: linear interpolation byweight(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.
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
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.
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 benchDependencies: @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.
- CSS Animations Level 1. W3C. —
@keyframes,animation-*properties, timing model. - CSS Easing Functions Level 2. W3C. —
linear()piecewise easing,steps()jump terms,cubic-bezier(). - CSS Transforms Module Level 2. W3C. —
matrix3d(), decomposition algorithm, quaternion interpolation. - CSSOM View Module. W3C. —
getComputedStyle, unit resolution for relative values. - Web Animations API. W3C. —
Element.animate(); the WAAPI delegation path. @mkbabb/value.js— CSS value parsing, color spaces, unit conversion, easing functions.@mkbabb/parse-that— Parser combinators for the@keyframesgrammar.- de Casteljau, P. (1959). Outillages méthodes calcul. — The recursive subdivision algorithm used for general Bezier curves.
