@vysmo/easings Library Math

Easings for the modern web.

A curated catalog of easings, parametric builders (spring, bezier, rough, wiggle, smooth, gravity, breathe), and modifiers (reverse, mirror, yoyo). CSS export via toCSSLinear. Pure math, zero platform assumptions.

  • 22 families
  • ~2 KB gzipped
  • 0 runtime deps
  • CSS export linear() built-in
$ pnpm add @vysmo/easings View on GitHub

Install

Pure math, zero runtime dependencies, SSR-safe at module load, tree-shakable to the byte. Pair with @vysmo/animations for the full duration / interpolation runtime, or use the curves with any animation library that accepts a function ease.

pnpm add @vysmo/easings npm install @vysmo/easings yarn add @vysmo/easings

One package, three subpath exports: @vysmo/easings (curves + builders + modifiers), @vysmo/easings/css (toCSSLinear, toCSSBezier, toCSSKeyframes), and @vysmo/easings/reduced-motion (prefersReducedMotion, respectReducedMotion). @vysmo/easings/parse is also exposed for GSAP- compatible string references.

Quick start

Every easing is a (t: number) → number function with metadata. Pass it to any animation library, sample it to a CSS linear() value, or build your own with defineEasing.

import { animate } from "@vysmo/animations";
import { power2Out, spring } from "@vysmo/easings";

// Every easing is just (t: number) => number — pass it to any
// animation library that accepts a function-shaped ease.
animate({
  from: 0,
  to: 1,
  duration: 600,
  ease: power2Out,
  onUpdate: (v) => element.style.opacity = String(v),
});

// Parametric easings expose .with(...) to build a one-shot variant
// without losing the metadata that makes types-check work.
const popSpring = spring.with({ stiffness: 220, damping: 18 });
animate({ from: 0, to: 1, ease: popSpring, duration: 700, /* ... */ });

Two patterns. Direct — import a named easing and pass it as ease. Parametric — call .with() on a parametric easing (back, elastic, steps, spring, bezier, rough, wiggle, slow, anticipate) to get a tuned variant. .with(...) is the only place params live — nothing is mutated, every variant is its own EasingFn.

Concepts

A short mental model before the API ref. Skip if you're already comfortable from the playground.

Easings are functions

Every export satisfies (t: number) => number with a frozen easingName for tooling. t is in [0, 1]; the return value is usually in [0, 1] too — though back, elastic, and spring may briefly exceed those bounds (overshoot / undershoot). That's by design.

Endpoint exactness by default

At t === 0 the result is 0; at t === 1 it's 1. Numerical drift in floating-point math can't make a button settle at 0.99998 instead of fully snapped — we clamp the output. Disable via defineEasing(..., { exactEndpoints: false }) for curves that intentionally don't hit (0, 0) or (1, 1) (e.g. steps(n, "start")).

Parametric vs constant

Constant easings (power2Out, sineInOut, …) are ready to use as-is. Parametric ones (back, elastic, spring, …) expose .with(partial) — you pass the params you want to override; the rest fall back to defaults. The result is a fresh EasingFn, not a mutation.

Modifiers compose freely

reverse, mirror, yoyo, chain, blend, compose, slice all take an EasingFn and return one. mirror turns any ease into an inOut; yoyo bakes a 0 → 1 → 0 round-trip into a single progress sweep. They compose with parametric .with(...) results, so you can do mirror(spring.with(...)).

CSS export

toCSSLinear(ease, samples) from @vysmo/easings/css samples any curve into a CSS Easing Functions Level 2 linear() value. toCSSBezier emits the exact cubic-bezier() form for bezier curves (no sampling). The CSS subpath stays in its own export so consumers using only the JS curves don't ship the sampler.

SSR-safe at import

No DOM access at module load. The only DOM-touching helper is prefersReducedMotion() from @vysmo/easings/reduced-motion, and it short-circuits to false when window is undefined. Safe to import server-side without guards.

API reference

The whole surface fits in five tables. Every entry is tree-shakable — only what you import ships.

The shape — EasingFn

Every export is a function with easingName metadata. Parametric easings extend the shape with frozen defaults and a .with(partial) builder. defineEasing / defineParametricEasing wrap your own math in the same contract.

// The atomic contract — every export, modifier, and builder result
// satisfies this shape.
type EasingFn = ((t: number) => number) & {
  readonly easingName: string;
};

// Parametric easings extend it with a default param map and a
// .with(partial) builder that returns a fresh EasingFn.
type ParametricEasing<P extends object> = EasingFn & {
  readonly defaults: Readonly<P>;
  with(params: Partial<P>): EasingFn;
};

Core curves

The eight named families — each in three variants (in / out / inOut) plus linear. power2* through power4* are aliased as cubic* / quart* / quint* for GSAP / Penner-curve familiarity.

import {
  // Core curves — all variantsInOut: in / out / inOut
  linear, none,
  power1In, power1Out, power1InOut,        // quadratic (t²)
  power2In, power2Out, power2InOut,        // cubic    (t³)  — alias: cubicIn / cubicOut / cubicInOut
  power3In, power3Out, power3InOut,        // quartic  (t⁴)  — alias: quartIn / quartOut / quartInOut
  power4In, power4Out, power4InOut,        // quintic  (t⁵)  — alias: quintIn / quintOut / quintInOut
  sineIn, sineOut, sineInOut,              // sinusoidal
  circIn, circOut, circInOut,              // circular arc
  expoIn, expoOut, expoInOut,              // 2^t exponential
  // Parametric
  backIn, backOut, backInOut,              // .with({ overshoot })
  elasticIn, elasticOut, elasticInOut,     // .with({ amplitude, period })
  bounceIn, bounceOut, bounceInOut,        // ball-drop pattern
  steps,                                   // .with({ count, position })
} from "@vysmo/easings";

Builders

Parametric factories that build a curve from numeric inputs. spring is physics-based; bezier is exact CSS-style; rough / wiggle add noise for gritty motion; custom takes a sparse keyframe list and interpolates piecewise.

import {
  bezier,         // (p1x, p1y, p2x, p2y) → CSS-style cubic-bezier
  spring,         // .with({ stiffness, damping, mass, velocity })
  rough,          // .with({ strength, points, taper, seed })
  wiggle,         // .with({ wiggles, type })
  slow,           // .with({ linearRatio, power })
  anticipate,     // anticipate / anticipateIn / anticipateOut / anticipateInOut
  expoScale,      // (startScale, endScale) → even-feeling scale ramps
  custom,         // (points: CustomPoint[]) → piecewise from your own data
} from "@vysmo/easings";

// Spring presets if you'd rather grab a known feel:
import {
  gentleSpring, wobblySpring, stiffSpring, slowSpring, molassesSpring,
  SPRING_PRESETS,
} from "@vysmo/easings";

Modifiers

Function-to-function transforms. reverse, mirror, yoyo are the everyday ones. chain stitches multiple easings end-to-end with weights. blend linearly interpolates between two; slice reads a sub-range and renormalises.

import {
  reverse,    // f(t) → 1 - f(1 - t)            — flip in/out feel
  mirror,     // f(t) for t<0.5; 1 - f(2-2t) → makes any ease into an inOut
  yoyo,       // 0 → 1 → 0 round-trip in one progress sweep
  chain,      // chain([{ ease: a, duration: 0.7 }, { ease: b, duration: 0.3 }])
  blend,      // blend(a, b, weight) where weight is 0..1
  compose,    // compose(a, b) → b(a(t))        — function composition
  slice,      // slice(ease, fromT, toT) → renormalised sub-curve
} from "@vysmo/easings";

// Modifiers take an EasingFn in and return an EasingFn out — they
// compose freely with parametric .with(...) results.
const wobbleIn = mirror(spring.with({ stiffness: 320 }));

CSS export — /css

Three pure functions. toCSSLinear samples any curve into a CSS linear() value; toCSSBezier emits an exact cubic-bezier(); toCSSKeyframes pre-bakes the eased values into a @keyframes block driving any CSS property.

import { toCSSLinear, toCSSBezier, toCSSKeyframes } from "@vysmo/easings/css";

// Sample any curve to a CSS Easing Functions Level 2 linear() value.
const value = toCSSLinear(spring.with({ stiffness: 220 }), 32);
//   → "linear(0, 0.092, 0.31, 0.62, 0.88, 1.04, 1.02, 1, ...)"
document.documentElement.style.setProperty("--ease-pop", value);

// Exact (no sampling) for cubic-bezier curves.
toCSSBezier(0.42, 0, 0.58, 1);
//   → "cubic-bezier(0.42, 0, 0.58, 1)"

// Pre-bake the easing into @keyframes when you'd rather not pay the
// linear() cost on the GPU side.
toCSSKeyframes(
  "slideIn",
  "transform",
  (eased) => `translateX(${eased * 100}%)`,
  bounceOut,
  20,
);

Parsing & reduced motion

parseEasing(spec) accepts GSAP-compatible string references — useful when easings come from a config file or design tool. prefersReducedMotion() is the OS-level check; respectReducedMotion(ease, fallback?) wraps an easing so it falls back to linear (or your choice) when reduced motion is set.

import { parseEasing } from "@vysmo/easings/parse";

// GSAP-style string references — useful for config-driven setups.
parseEasing("power2.out");                   // → power2Out
parseEasing("back.out(2)");                  // → backOut.with({ overshoot: 2 })
parseEasing("elastic.out(1.2, 0.4)");        // → elasticOut.with({ ... })
parseEasing("steps(5, start)");              // → steps.with({ count: 5, position: "start" })
parseEasing("cubic-bezier(.42, 0, .58, 1)"); // → bezier(...)

import { prefersReducedMotion, respectReducedMotion } from "@vysmo/easings/reduced-motion";

// Detect once at the start of an animation.
if (prefersReducedMotion()) ease = linear;

// Or wrap inline — falls back to linear when the user has reduced
// motion enabled.
animate({
  ease: respectReducedMotion(spring.with({ stiffness: 220 })),
  duration: 600,
});

Catalog

22 families across 46 shipping idents — every export with its variants and parameters. Pick a family to inspect its surface; same data the playground uses.

linear

No easing — constant velocity.

Export
linear

No params.

power1

Quadratic (t²). Mildest power curve.

Variants
power1Inpower1Outpower1InOut

No params.

power2 (cubic)

Cubic (t³). Workhorse default — CSS's ease is close to power2.out.

Variants
power2Inpower2Outpower2InOut

No params.

power3 (quart)

Quartic (t⁴). More pronounced easing.

Variants
power3Inpower3Outpower3InOut

No params.

power4 (quint)

Quintic (t⁵). Dramatic acceleration.

Variants
power4Inpower4Outpower4InOut

No params.

sine

Sinusoidal (cos/sin-based). Soft and natural.

Variants
sineInsineOutsineInOut

No params.

circ

Circular arc. Sharp at one end, flat at the other.

Variants
circIncircOutcircInOut

No params.

expo

Exponential (2^). Extreme — near-zero until the last moments, or vice versa.

Variants
expoInexpoOutexpoInOut

No params.

smooth

Hermite smoothstep (3t² - 2t³). Zero velocity at both endpoints — smoother joins than power2.inOut for chained animations.

Variants
smoothInsmoothOutsmoothInOut

No params.

back

Overshoots the target then returns. Classic 'pull-back' motion.

Variants
backInbackOutbackInOut
Prop
Type
Default
Values
overshoot
number
1.70158
0 – 5 · step 0.01
elastic

Spring-like oscillation. Two knobs: amplitude (peak height) and period (frequency).

Variants
elasticInelasticOutelasticInOut
Prop
Type
Default
Values
amplitude
number
1
0.1 – 5 · step 0.01
period
number
0.3
0.05 – 1 · step 0.01
bounce

Ball-drop pattern. Four decaying bounces.

Variants
bounceInbounceOutbounceInOut

No params.

steps

Discrete staircase. CSS-compatible step positions.

Export
steps
Prop
Type
Default
Values
count
number
5
1 – 30 · step 1
position
enum
end
end · start · none
bezier

CSS cubic-bezier. Paste a cubic-bezier() value from CSS or design tools.

Export
bezier
Prop
Type
Default
Values
p1x
number
0.42
0 – 1 · step 0.01
p1y
number
0
-2 – 2 · step 0.01
p2x
number
0.58
0 – 1 · step 0.01
p2y
number
1
-2 – 2 · step 0.01
spring

Physics-based spring. Stiffness × damping × mass = feel.

Export
spring
Prop
Type
Default
Values
stiffness
number
170
10 – 1000 · step 5
damping
number
26
1 – 100 · step 1
mass
number
1
0.1 – 10 · step 0.1
velocity
number
0
-20 – 20 · step 0.5
rough

Jittery noise layered on a base curve. Great for gritty / handheld feel.

Export
rough
Prop
Type
Default
Values
strength
number
0.15
0 – 0.5 · step 0.01
points
number
20
5 – 60 · step 1
taper
enum
both
none · in · out · both
seed
number
42
1 – 999 · step 1
wiggle

Oscillates between -1 and 1 across [0, 1]. For shake / vibration effects.

Export
wiggle
Prop
Type
Default
Values
wiggles
number
10
1 – 30 · step 1
type
enum
easeOut
easeOut · easeInOut · anticipate · uniform
slow

Linear middle section flanked by power-eased edges. For slow-motion feel.

Export
slow
Prop
Type
Default
Values
linearRatio
number
0.7
0 – 1 · step 0.01
power
number
0.7
0 – 5 · step 0.1
anticipate

Character-animation wind-up: dips backward (in), overshoots past target (out), or both (inOut). Framer Motion style.

Variants
anticipateInanticipateOutanticipateInOut
Prop
Type
Default
Values
overshoot
number
1.525
0 – 5 · step 0.01
expoScale

Maps [0, 1] for scale animations crossing large ratios (e.g. 0.1→100) so motion feels even.

Export
expoScale
Prop
Type
Default
Values
startScale
number
1
0.01 – 100 · step 0.1
endScale
number
100
0.01 – 1000 · step 1
gravity

Continuously parameterised power-in. weight=0 floats (linear), weight=1 ≈ Earth fall (quadratic / power1.in), weight=2 cubic, higher = molasses. One knob instead of picking between named power curves.

Export
gravity
Prop
Type
Default
Values
weight
number
1
0 – 3 · step 0.05
breathe

Continuous oscillation in [0, 1]. cycles=0.5 = single inhale (0 → 1). cycles=1 = inhale + exhale. For idle / ambient animations on opacity, scale, anything in [0, 1].

Export
breathe
Prop
Type
Default
Values
cycles
number
1
0.5 – 8 · step 0.5

Recipes

Common patterns. Each one stands alone — copy, adapt, ship.

Spring as a CSS variable

Define the spring in JS once, expose it as a CSS variable, drive every native CSS animation from there. No JS on the hot path at runtime.

import { spring, toCSSLinear } from "@vysmo/easings";

// Define the curve in JS, expose it as a CSS variable, drive native
// CSS animations from there. No JS in the hot path at runtime.
const popSpring = spring.with({ stiffness: 240, damping: 14 });

document.documentElement.style.setProperty(
  "--ease-pop",
  toCSSLinear(popSpring, 32),
);

/* in your stylesheet */
.button {
  transition: transform 0.4s var(--ease-pop);
}
.button:hover { transform: scale(1.06); }

Designer-to-code hand-off

Designer ships a cubic-bezier() from Figma / Framer. Same curve in code via bezier(...), same curve back in CSS via toCSSBezier(...). No rounding drift through the chain.

import { bezier, toCSSBezier } from "@vysmo/easings";

// Designer drops a Figma cubic-bezier in Slack. Same curve in code,
// same curve in CSS — no rounding drift through a hand-rebuild.
const ease = bezier(0.34, 1.56, 0.64, 1);

console.log(toCSSBezier(0.34, 1.56, 0.64, 1));
//   → "cubic-bezier(0.34, 1.56, 0.64, 1)"

animate({ from: 0, to: 1, ease, duration: 500, /* ... */ });

Multi-stage motion via chain

Two-stage motion in one ease: snap fast on the way up, settle with a bounce. Weights are normalised durations — a 0.7 / 0.3 split spends 70% of the animation on the first easing.

import { chain, power2Out, bounceOut } from "@vysmo/easings";

// Two-stage motion: snap up fast, then settle with a small bounce.
// `duration` is each segment's share of the total — they're
// normalised, so the absolute units don't matter.
const snapAndBounce = chain([
  { ease: power2Out, duration: 0.7 },
  { ease: bounceOut, duration: 0.3 },
]);

Define your own ease

defineParametricEasing wraps your math in the same shape as the built-ins — frozen defaults, a typed .with(partial) builder, and the exactEndpoints guarantee.

import { defineParametricEasing } from "@vysmo/easings";

// Build your own parametric easing with the same metadata + endpoint
// guarantees as the built-ins. Returns a ParametricEasing<P> with a
// frozen `defaults` and a `.with(partial)` builder.
export const wobble = defineParametricEasing(
  "wobble",
  { strength: 1, frequency: 4 },
  ({ strength, frequency }) => (t) =>
    t + strength * Math.sin(t * frequency * Math.PI * 2) * (1 - t),
);

const tighter = wobble.with({ strength: 0.4 });

FAQ & gotchas

The handful of things that catch people the first time they ship a curve.

Why does my back.out overshoot past 1?

That's the whole point — back, elastic, spring, and anticipate intentionally exceed the [0, 1] output range to give motion the wind-up and overshoot character that makes UI feel alive. If you need the eased value clamped (e.g. driving an opacity), wrap with Math.max(0, Math.min(1, ease(t))) at the call site, or use a non-overshoot family (power2Out, circOut, expoOut).

Why does steps(n, "start") not start at 0?

By spec, steps(n, "start") jumps to the first step at t === 0. This is the only built-in that intentionally violates the (0, 0) / (1, 1) endpoint guarantee. Inside the package it's wrapped with defineEasing(..., { exactEndpoints: false }) so the clamp doesn't fight the spec.

How do I respect prefers-reduced-motion?

Use respectReducedMotion(ease, fallback?) from @vysmo/easings/reduced-motion. It returns fallback (default linear) when the user has reduced motion enabled, otherwise the original. Evaluated at call time — not reactive to preference changes during a single animation; re-evaluate per new animation in long-running apps.

import { respectReducedMotion, spring } from "@vysmo/easings";

const ease = respectReducedMotion(spring.with({ stiffness: 220 }));
// → linear (when prefers-reduced-motion: reduce)
// → spring (otherwise)

animate({ ease, /* ... */ });

When should I use CSS linear() vs cubic-bezier()?

cubic-bezier() is exact — use it for any ease that is a cubic bezier (i.e. came from bezier(...) or a design tool). For springs, bouncing, stepped, or modifier-wrapped curves, sample to linear() via toCSSLinear. 32 samples is a good default; bump to 64 only if you can see staircasing on a slow / long animation.

Bundle size — what do I actually ship?

The library is sideEffects: false and every easing lives in its own module. With tree-shaking enabled, you only ship what you import. The whole catalog is ~2 KB gzipped; a single curve like power2Out is a few hundred bytes. spring, rough, wiggle, and the CSS sampler add a bit more — numbers in the README and verified by scripts/check-bundle-size.mjs.

Can I use these with GSAP / Motion / Anime.js?

Yes — an EasingFn is just (t: number) => number at the call site. Hand it to any library that accepts a function-shaped ease. GSAP consumers can also use parseEasing("power2.out") and pass the result directly. The only catch: libraries that want a string (not a function) won't accept these without the string-style adapter.