A Typescript sourced Tween engine forked from the excellent @tweenjs/tweenjs.
Tween class for creating most simple tween objects, with most essential controls over the update loop and simple methods for sequencing.Timeline class which enables an advanced control over the update loop, most importantly a per-property control for duration and easing.A quick React demo or Solid demo.
Timeline class for advanced sequencing/overlapsnpm install @thednp/tween
pnpm add @thednp/tween
deno add @thednp/tween
bun add @thednp/tween
import { Tween, Easing } from '@thednp/tween';
// find some target
const target = document.getElementById('my-target');
// define a tween
const tween = new Tween({ x: 0 })
.duration(1.5) // duration/delay accept seconds (e.g., 1.5 = 1.5s)
.onUpdate((obj, elapsed, eased) => {
// update App state
// OR manipulate the DOM directly
Object.assign(target.style, { translate: obj.x + "px"});
// monitor progress of the tween
console.log(`Tween progress: ${Math.floor(elapsed * 100)}%`)
// do other stuff with the `eased` value
});
// override any value on the fly
const moveRight = () => tween
.from({ x: 0 }) // override/reset start values
.to({ x: 150 }) // override end values
.easing(Easing.Quadratic.Out) // set a special easing function for every case
.duration(1.5) // set duration as well in seconds
.start(); // start the tween
const moveLeft = () => tween
.to({ x: -150 }) // set a different to
.easing(Easing.Elastic.Out) // override easing
.duration(1.5) // override duration in seconds
.start(); // start the tween
// trigger any time
const button1 = document.getElementById('my-button-1');
const button2 = document.getElementById('my-button-2');
button1.onclick = moveRight;
button2.onclick = moveLeft;
// The engine does requestAnimationFrame/cancelAnimationFrame for you
import { Timeline, Easing } from '@thednp/tween';
// find some target
const target = document.getElementById('my-target');
// define a timeline
const myTimeline = new Timeline({ x: 0, y: 0 })
.to({ x: 150, duration: 2.5, easing: Easing.Elastic.Out })
.to({ y: 150, duration: 1.5, easing: Easing.Elastic.Out }, "-=1")
.onUpdate((obj, elapsed) => {
// update App state
// OR manipulate the DOM directly
Object.assign(target.style, {
translate: obj.x + "px " + obj.y + "px",
});
// monitor progress of the tween
console.log(`Timeline progress: ${Math.floor(elapsed * 100)}%`)
});
// trigger any time
const button = document.getElementById('my-button-1');
button.onclick = myTimeline.play();
// The engine does requestAnimationFrame/cancelAnimationFrame for you
To use Tween or Timeline with frameworks please check React, SolidJS (more to come).
This Tween version is very small, maybe half the size of the current original version. It was developed to create easy to use hooks for UI frameworks like Solid/React and enable customizable animations.
In fact this is closer to the earlier versions of the original TWEEN.Tween.js.
This package comes with Timeline, which works like a regular Tween under the hood, but it provides additional control methods like pause(), resume() seek().
Both Tween and Timeline have a static method to add custom interpolators, which is a unique way to extend beyond the original design and very different from the original library.
yoyo, repeat, repeatDelay, chain and onRepeat or onEveryStart, onFirstStart are not implemented;duration() and delay() methods accept values in seconds and convert them to milliseconds; pause() and resume() methods have not been implemented in Tween, but they are implemented in Timeline;requestAnimationFrame / cancelAnimationFrame calls is automatic and is integrated in the Tween methods;onUpdate callback also uses the value calculated by the easing function as the third parameter of your callback;Tween guideTimeline guideTween / Timeline with React with custom hooks.Tween / Timeline with SolidJS with primitives.@thednp/tween is intentionally designed as a lightweight, state-first tweening engine. Here's why certain choices were made and how the system works under the hood.
Most classic animation libraries (GSAP, KUTE.js, Velocity.js) are imperative: they read current DOM styles/attributes at runtime, parse them, compute differences, and write back updates.
@thednp/tween keeps with the original Tween.js, which is the opposite approach — it's purely state-driven:
.start() / .play()) Advantages:
Trade-offs:
This makes @thednp/tween feel more like a reactive state interpolator than a traditional DOM tweener.
All animations share one single requestAnimationFrame loop managed by Runtime.ts.
.start() / .play(), the instance is added to a global QueueRuntime() runs every frame → calls .update(time) on every queued item.update() returns false (finished/stopped), the item is removed from the QueueQueue becomes empty → cancelAnimationFrame is called automatically → loop stops completelyBenefits:
This shared loop is why you never need to worry about starting/stopping individual requestAnimationFrame calls.
All updates are async by nature:
.start() / .play() → instance queuedrAF tick → Runtime() calls .update(time) → interpolates → calls onUpdateThis means:
.to(), .duration(), etc. during an animation — changes apply on the next frame@thednp/tween is SSR-safe out of the box:
requestAnimationFrame / cancelAnimationFrame are only called in browser (via Runtime())now() defaults to performance.now() or Date.now() — safe fallbacks in NodeJust make sure to not call tween.start() / timeline.play() during SSR (e.g. wrap in if (typeof window !== 'undefined') or use useEffect).
| Feature/Aspect | Classic (GSAP/KUTE/Velocity) | @thednp/tween |
|---|---|---|
| Animation target | DOM elements & CSS | Any JS object (state, data, Canvas, etc.) |
| Value reading | Parses current style/attr at runtime | Captures current JS values at start |
| Performance on startup | Slower (parsing + computation) | Fastest (no parsing) |
| State-based UI compatibility | Requires extra glue code | Native — ideal for React/Solid/Vue/Svelte |
| Global vs per-instance config | Plugins/global easing | Per-instance .use() (recommended) |
| Bundle size | Larger (DOM utils, parsing, plugins) | Very small (~2–4 KB minzipped) |
| Runtime loop | Per-tween or managed | Single shared global loop (most efficient) |
.stop() + .startFromLast() for pause-like behavior (keeps it simple)Tween (use Timeline for sequencing/repeats).use('prop', fn) (prevents conflicts in large apps)setNow() allows perfect time control in tests (override now() to fake progression)We believe this combination of small size, state-first design, shared loop, and per-instance flexibility makes @thednp/tween uniquely suitable for modern component-based UIs in 2026.
This is a work in progress. For any issue or unclear guides, please file an issue and help make this guide better. Or feel free to submit a PR! Thank you!
How to contribute:
@thednp/tween is released under MIT License.