Tensum is a GSAP plugin backed by an analytical spring solver. GSAP owns the timeline, scheduling, and context lifecycle. When GSAP renders the plugin, Spring samples position and velocity from absolute time and writes the sampled property values.
The repository contains one public package:
tensum
The public spring configuration has no required duration. Duration comes from the physical state and settling tolerances.
pnpm add tensum gsapBoth packages are required. GSAP is a peer dependency so an application and the plugin use the same GSAP instance.
import { gsap } from "gsap";
import { TensumPlugin, springPresets } from "tensum";
gsap.registerPlugin(TensumPlugin);Use the registered timeline.spring() effect when a spring's derived
duration affects timeline layout. The effect calculates the duration before
GSAP inserts the tween, so sequential children, staggered targets, and nested
timelines have their final timing immediately.
const timeline = gsap.timeline();
timeline
.spring(element, {
x: 320,
from: { x: 0 },
parameters: { mass: 1, stiffness: 180, damping: 24 },
})
.spring(element, {
x: 80,
from: { x: 320 },
parameters: { mass: 1, stiffness: 240, damping: 26 },
});from declares the state from which preflight should calculate. Omit it when
the target already has the right value while the effect is being created. Set
it when an earlier timeline child will change that value before this spring
starts. Use the existing velocity option when the future state also has a
known velocity.
Pass ordinary GSAP options through tween. The effect owns duration and
ease. Finite springs support options such as stagger, repeat,
and yoyo. An unsettled spring using continue owns an infinite driver instead;
it sets repeat: -1 and samples analytical total time continuously.
const entrance = gsap.timeline().spring(cards, {
y: 0,
from: { y: -24 },
parameters: springPresets.snappy(),
tween: { stagger: 0.06, repeat: 1, yoyo: true },
});
masterTimeline.add(entrance, 0.4);createMotionSpringTween(targets, vars) exposes the same preflight step when
code needs the tween itself rather than the extended timeline method.
springTo() is the direct API for cases that need a controller rather than a timeline child.
import { springTo } from "tensum";
const animation = springTo(element, {
x: 600,
rotation: "30deg",
velocity: { x: 1250 },
spring: {
mass: 1,
stiffness: 180,
damping: 24,
settle: { position: 0.1, velocity: 0.1 },
},
properties: {
rotation: { damping: 30, velocity: 90 },
},
});
animation.pause();
animation.seek(0.2);
animation.resume();Built-in transform properties are x, y, scale, and rotation. Use
targets for other numeric GSAP properties or adapters for custom read/write
behavior. Numeric strings may contain one unit, such as 24px or 30deg. A
unit mismatch throws before animation starts. DOM x/y values default to
pixels and DOM rotation defaults to degrees; ordinary object properties remain
unitless numbers unless their value or configuration explicitly supplies a
unit.
Starting another springTo() animation or constructing a timeline spring
on the same target and property performs an automatic velocity-preserving
handoff from the state that exists at construction time. Provide from and
velocity for a state that will only exist later in the timeline.
After normal completion, the last analytical state remains available for a
later Tensum handoff. This includes the non-zero velocity of an unsettled
spring stopped at maxDuration. If application code writes a different value
after completion, the next implicit handoff detects that change, discards the
terminal history, and starts from the value currently on the target. An explicit
from always supplies the effect's starting snapshot.
Supply physical parameters directly:
const parameters = {
mass: 1,
stiffness: 180,
damping: 24,
};Or derive them from product-facing controls:
import { springParameters, springPresets } from "tensum";
const tuned = springParameters.fromPerceptualDuration({
duration: 0.5,
bounce: 0.15,
});
const snappy = springPresets.snappy();All time values are seconds. Velocity is property units per second. The solver is unit-agnostic; x may use pixels while rotation uses degrees.
A damped spring approaches equilibrium asymptotically. The plugin treats a spring as physically settled when both conditions remain true for its future tail:
abs(position - target) <= positionEpsilon
abs(velocity) <= velocityEpsilon
Absolute tolerances allow a longer movement to take longer to settle even when its normalized response is the same.
An undamped moving spring never reaches physical rest. Choose an unsettled policy explicitly when this is possible:
stopfreezes atmaxDurationand is the default;snapwrites the target atmaxDuration;continuekeeps an infinite GSAP clock running;errorrejects the animation.
For a finite driver, logical completion is clamped to its completion boundary.
This includes the maxDuration boundary for stop and snap. continue
retains the requested perceptual duration. onSettle does not run for a spring
that failed to settle. Effect lifecycle callbacks are target-scoped: an array
tween calls the same callback once per target, and each snapshot contains only
that target's spring states.
Finite repeats and yoyo playback use cycle-local spring time. A yoyo handoff
reverses the inherited velocity as well as the position path. Killing one
property removes only that spring track; killing the last track releases the
internal driver. If the killed track made an unsettled continue tween
infinite, the remaining duration and original repeat count are restored.
Calling invalidate() keeps the snapshot and duration captured when the effect
was created; construct a new effect tween to preflight different inputs. Kill,
interrupt, and context revert release active ownership. As with native GSAP
tweens, context revert restores the baseline captured when the effect was
constructed.
The root entry point also exports createSpring, parameter converters, velocity
helpers, spring values, keyframes, inertia, additive composition, and vector
springs.
Advanced CSS and coupled-system utilities remain package subpaths:
import { springToCSSLinear } from "tensum/css";
import { createCoupledSpringSystem } from "tensum/coupled";The CSS exporter treats maxError as a hard normalized-progress bound for the
serialized linear() function. If duration, precision, maxDepth, or
maxSamples make that bound impossible, it throws instead of returning an
under-sampled curve.
packages/tensum one public GSAP plugin package
apps/site Svelte product site and interactive playground
Inside the package, the analytical solver stays separate from the GSAP adapter so sampling remains deterministic. This is an implementation boundary, not another public library.
pnpm install
pnpm dev
pnpm test
pnpm check
pnpm lint
pnpm build
pnpm benchmark
pnpm release:checkThe repository toolchain requires Node.js 22.12 or newer. The published package is additionally tested on Node.js 20.19.x; its complete runtime, browser, TypeScript, and GSAP matrix is defined in the platform support baseline.
pnpm release:check builds every workspace, runs all checks, validates the
complete named value and type export surface, validates the package manifest,
packs a tarball, and installs that tarball in a temporary consumer project. It
does not publish or tag a release.
The package is ESM-only. TypeScript consumers should use node16, nodenext,
or bundler module resolution. CommonJS code can load it with dynamic
import(), but require() is intentionally unsupported. Version 0.x may
contain breaking API changes.
See the package README for the full public API and the changelog for release status.
MIT, copyright 2026 Motion Core. See LICENSE.