A lightweight, high-performance JavaScript animation library for the browser. Tween any numeric property of any object — DOM elements, canvas objects, Three.js vectors, custom models — with a rich ease system, composable tweens, action tweens, and full CSS 3D transform interpolation.
No dependencies. No build step. ~65 KB gzipped.
Robust Tweens for Actionscript
Available globally as BJS, BTW, or BetweenJS — use whichever reads best to you.
BetweenJS is one of the best tools to quickly prototype and refine advanced animations — start basic and layer in power as you go, with minimal edits between each step.
// --- 1. One-liner shortcut ---
BJS.to(el, { left: 500 }, 2, Quad.easeOut).play();
// --- 2. Same with create() — add features by editing the options object ---
BJS.create({
target: el,
to: { left: 500, top: 300 }, // multi-property
time: 2,
ease: Quad.easeOut,
delay: 0.5, // wait before starting
repeat: 2, // repeat count (-1 = infinite)
cuepoints: { left: [250, 400] }, // bezier waypoints
transform: { translateX: 200, rotate: 45 }, // GSAP-style 2D/3D transforms
onComplete: function(){ /* ... */ }
}).play();
// --- 3. Compose: serial() / parallel() for sequences and simultaneity ---
BJS.serial(
BJS.to(el, { left: 500 }, 1),
BJS.func(function(){ console.log('midpoint'); }), // action tween
BJS.to(el, { top: 300 }, 1)
).play();
// --- 4. Fluent chaining: decorate, then play ---
BJS.to(el, { left: 500 }, 2).reverse().delay(0.3).play();
// --- 5. Action tweens block serial chains for real sequencing ---
BJS.serial(
BJS.load('/data.json', function(d){ window.data = d; }),
BJS.to(el, { left: 500 }, 1, Quad.easeOut)
).play();
// --- 6. Physical eases compute their own duration ---
BJS.create({ target: el, to: { left: 500 }, ease: Physical.uniform(200, 60) }).play();Why you'll love this workflow :
Every step above is a trivial edit from the previous one. You never rewrite — you just add options, wrap in serial(), or slap on a decorator. The ticker's yield system gives you 60fps+ performance with automatic pause on tab-switch, zero garbage, and AnimationTicker-backed frame accuracy.
BetweenJS is a single-file ES5 library. Include it with a <script> tag:
<script src="betweenjs.js"></script>On load it creates the global BJS (also BTW and BetweenJS). All classes (eases, tickers, etc.) are on window.
If you have a require() polyfill, it also exports via module.exports.
A tween animates a target object's properties from a source value to a destination value over time.
var tw = BJS.create({
target: myElement, // DOM element, canvas object, anything
from: { left: 0 }, // optional — if omitted, reads current value
to: { left: 500 }, // required
time: 2, // seconds
ease: Quad.easeOut, // easing function
onComplete: function(){ console.log('done!'); }
});
tw.play();BetweenJS uses requestAnimationFrame via its own EnterFrameTicker. You never need to call a render loop — it starts automatically when the first tween plays.
Any object with numeric properties works: DOM elements, canvas drawables, Three.js objects, plain JS objects, jQuery collections.
CSS properties can be written either way — BJS auto-converts camelCase to dash notation:
// Both work identically:
BJS.to(el, { backgroundColor: { r: 255, g: 0, b: 0 } }, 1).play();
BJS.to(el, { 'background-color': { r: 255, g: 0, b: 0 } }, 1).play();Use whichever you prefer.
BJS interpolates between a source and destination value. If the initial CSS state doesn't match how you declare properties in BJS, the tween will jump at completion.
Bad — shorthand vs specific property mismatch:
#box { background: rgb(255, 0, 0); }// This will NOT tween smoothly — 'background' shorthand != 'background-color'
BJS.to(el, { backgroundColor: { r: 0, g: 0, b: 0 } }, 1).play();
// → element stays red for 1s, then SNAPS to black onCompleteGood — be homogeneous:
#box { background-color: rgb(255, 0, 0); }BJS.to(el, { backgroundColor: { r: 0, g: 0, b: 0 } }, 1).play();
// → smoothly fades from red to blackIf a CSS property isn't explicitly set, getComputedStyle may return auto or none — which BJS cannot interpolate from. Always declare a baseline in CSS or inline.
#box {
opacity: 1; /* explicit baseline */
width: 100px;
}<div id="box" style="opacity:1; width:100px;"></div>// Now these work reliably:
BJS.to(el, { opacity: 0 }, 1).play();
BJS.to(el, { width: 500 }, 1).play();If you animate with % units, make sure the CSS baseline also uses %. Mixing % with px or a non-unit initial value will break interpolation.
/* Good — baseline uses % */
#box { width: 50%; left: 0%; }BJS.to(el, { 'width::%': 100, 'left::%': 50 }, 1).play();If the initial CSS uses px but you animate with %, BJS won't know how to bridge the two — the tween will jump. Stick to one unit system throughout.
The universal tween factory. Returns a Tween (or a ParallelTween if targeting multiple objects).
| Option | Type | Default | Description |
|---|---|---|---|
target |
Element∣Array∣jQuery |
required | Target(s) to animate |
to |
Object |
required | Destination property values |
from |
Object |
current | Source property values |
time |
Number |
1 |
Duration in seconds |
ease |
Function |
Linear.easeIn |
Easing function |
delay |
Number |
0 |
Seconds to wait before starting |
cuepoints |
Object |
— | Bezier waypoints, e.g. { left: [250, 400], top: [50, 300] } |
repeat |
Number |
0 |
Repeat count (0 = no repeat, -1 = infinite) |
onStart |
Function |
— | function() — fires when tween starts |
onUpdate |
Function |
— | function() — fires each frame |
onComplete |
Function |
— | function() — fires when done |
onStop |
Function |
— | function() — fires when stopped manually |
onPlay |
Function |
— | function() — fires each time tween plays |
transform |
Object |
— | GSAP-style transform shorthand (see CSS Transform) |
Examples:
// Basic
BJS.create({ target: el, to: { left: 500 }, time: 2 }).play();
// With from values
BJS.create({ target: el, from: { left: 0 }, to: { left: 500 }, time: 2 }).play();
// With transform
BJS.create({ target: el, transform: { translateX: 200, rotate: 45 }, time: 1.5 }).play();
// Multiple targets — automatically creates a ParallelTween
BJS.create({ target: [el1, el2, el3], to: { opacity: 0 }, time: 1 }).play();Positional shortcut. to and from are objects.
BJS.tween(el, { left: 500, top: 300 }, { left: 0, top: 0 }, 2, Quad.easeOut).play();Animate to destination values (reads current as source).
BJS.to(el, { left: 500, top: 300 }, 2, Quad.easeOut).play();Animate from source values to current values.
BJS.from(el, { left: 500, opacity: 0 }, 2, Cubic.easeIn).play();Set properties instantly without animation. Returns a one-frame tween.
BJS.apply({ target: el, to: { opacity: 0.5 } });Immediately set properties on target. No tween created.
BJS.instant(el, { opacity: 0, display: 'none' });Animate along a bezier path defined by cuepoint arrays.
BJS.bezier(
el,
{ left: 800, top: 400 }, // destination
{ left: 100, top: 100 }, // source
{ left: [300, 500], top: [50, 300] }, // cuepoints (arrays of intermediate values)
3, Quad.easeInOut
).play();Bezier with auto-sourced current values.
Bezier auto-targeted to current values.
Tween with a Physical ease. The ease argument should be a Physical instance.
BJS.physical(el, { left: 500 }, { left: 0 }, Physical.uniform(200, 60)).play();Physical tween, source from current values.
Physical tween, target from current values.
Physical tween that applies + draws at a specific time without animating.
Animate multiple targets with staggered delay.
BJS.stagger(
document.querySelectorAll('.item'),
{ left: 500 },
{ time: 0.5, staggerDelay: 0.1, ease: Quad.easeOut }
).play();Options accepts: time, ease, from, staggerDelay, startDelay, and all event handlers.
Play tweens one after another. Returns a SerialTween.
BJS.serial(
BJS.to(el, { left: 500 }, 1, Quad.easeOut),
BJS.to(el, { top: 300 }, 1, Quad.easeOut),
BJS.to(el, { left: 100, top: 100 }, 1, Quad.easeOut)
).play();Same as serial() but takes an array.
var tweens = [
BJS.to(el, { left: 500 }, 1),
BJS.to(el, { top: 300 }, 1)
];
BJS.serialTweens(tweens).play();Play tweens simultaneously. Returns a ParallelTween.
BJS.parallel(
BJS.to(el1, { left: 500 }, 2, Quad.easeOut),
BJS.to(el2, { left: 500 }, 1.5, Cubic.easeInOut)
).play();Same as parallel() but takes an array.
Returns a fluent timeline builder.
var tl = BJS.timeline();
tl.add(BJS.to(el, { left: 500 }, 1));
tl.add(BJS.to(el, { top: 300 }, 1));
tl.add(function(){ console.log('timeline done!'); });
tl.play();
// Properties: add(), play(), stop(), pause(), resume(),
// onComplete(cb), getDuration(), seek(time)Decorators wrap a tween to modify its behavior. They can be chained.
Play the tween backwards.
BJS.reverse(BJS.to(el, { left: 500 }, 2)).play();Play only a portion of the tween's timeline.
// Play only the middle 50% of the timeline
BJS.slice(BJS.to(el, { left: 500 }, 2), 0.25, 0.75, true).play();
// Play from 0.5s to 1.5s
BJS.slice(BJS.to(el, { left: 500 }, 2), 0.5, 1.5).play();Scale the tween's duration by a factor.
// Make a 2s tween take 4s
BJS.scale(BJS.to(el, { left: 500 }, 2), 2).play();
// Make a 2s tween take 0.5s
BJS.scale(BJS.to(el, { left: 500 }, 2), 0.25).play();Add leading and/or trailing delay to a tween.
// Wait 1s, then animate
BJS.delay(BJS.to(el, { left: 500 }, 2), 1).play();
// Wait 1s, animate, then wait 0.5s before signaling completion
BJS.delay(BJS.to(el, { left: 500 }, 2), 1, 0.5).play();Repeat a tween a number of times. Use -1 for infinite looping.
// Repeat 3 times
BJS.repeat(BJS.to(el, { left: 500 }, 2), 3).play();
// Loop forever
BJS.repeat(BJS.to(el, { left: 500 }, 2), -1).play();BetweenJS includes 11 ease families, each with 4 variants.
| Class | easeIn |
easeOut |
easeInOut |
easeOutIn |
|---|---|---|---|---|
Linear |
Linear | Linear | Linear | Linear |
Quad |
Accelerate | Decelerate | Both | Center-peak |
Cubic |
✦ | ✦ | ✦ | ✦ |
Quart |
✦ | ✦ | ✦ | ✦ |
Quint |
✦ | ✦ | ✦ | ✦ |
Sine |
✦ | ✦ | ✦ | ✦ |
Expo |
✦ | ✦ | ✦ | ✦ |
Circ |
✦ | ✦ | ✦ | ✦ |
Back |
Overshoot | Overshoot | Both | Both |
Bounce |
✦ | ✦ | ✦ | ✦ |
Elastic |
✦ | ✦ | ✦ | ✦ |
BJS.to(el, { left: 500 }, 2, Quad.easeOut).play();
BJS.to(el, { left: 500 }, 2, Cubic.easeInOut).play();
BJS.to(el, { left: 500 }, 2, Bounce.easeOut).play();
BJS.to(el, { left: 500 }, 2, Elastic.easeOut).play();Back — adjust overshoot with *With(s):
Back.easeInWith(2); // stronger overshoot
Back.easeOutWith(0.5); // milder overshoot
Back.easeInOutWith(1.5);Elastic — adjust amplitude and period with *With(a, p):
Elastic.easeOutWith(1, 0.3);
Elastic.easeInWith(2, 0.5);Physical eases model real-world physics. They take velocity/acceleration values in per-frame units at 60fps (not per-second).
// Constant velocity: 200 pixels per frame
Physical.uniform(200, 60);
// Constant acceleration: 50 px/frame², initial velocity 100 px/frame
Physical.accelerate(50, 100, 60);
// Exponential: grows by factor 1.1 per frame, threshold 0.01
Physical.exponential(1.1, 0.01, 60);Physical eases auto-compute their duration — you don't pass a time:
BJS.physical(el, { left: 500 }, { left: 0 }, Physical.uniform(200, 60)).play();
// Or via create:
BJS.create({
target: el,
to: { left: 500 },
from: { left: 0 },
ease: Physical.uniform(200, 60)
}).play();Physical classes:
| Factory Method | Class | Description |
|---|---|---|
Physical.uniform(v, fps) |
PhysicalUniform |
Constant velocity |
Physical.accelerate(accel, iv, fps) |
PhysicalAccelerate |
Constant acceleration |
Physical.exponential(factor, threshold, fps) |
PhysicalExponential |
Exponential decay/growth |
Create a custom ease from a function fn(t, b, c) where t is time (0..duration), b is start value, c is change (end - start).
var myEase = Custom.func(function(t, b, c){
return b + c * Math.pow(t / this.time, 3);
});
BJS.to(el, { left: 500 }, 2, myEase).play();Action tweens are tweens that perform side effects — calling functions, loading data, adding DOM children, etc. They can be used standalone or composed in serial()/parallel() chains where they block progression until complete.
Important: Action tweens created with BJS.timeout(), BJS.interval(), BJS.func(), and BJS.load() do not auto-play. Call .play() explicitly or nest in a serial/parallel chain.
Execute a function. Can include rollback logic. Good for callback-based sequencing in serial chains.
// Standalone
BJS.func(function(){ console.log('hello!'); }).play();
// In a serial chain — fires between animations
BJS.serial(
BJS.to(el, { left: 500 }, 1),
BJS.func(function(){ console.log('midpoint!'); }),
BJS.to(el, { left: 100 }, 1)
).play();
// With params
BJS.func(console.log, ['hello!']).play();Fire a callback after duration seconds. Cached — use BJS.clearTimeout(uid) to cancel.
BJS.timeout(2, function(){ console.log('2 seconds later'); }).play();
// In a serial chain — blocks for 2 seconds before next tween
BJS.serial(
BJS.timeout(2, function(){ console.log('wait over'); }),
BJS.to(el, { left: 500 }, 1)
).play();Fire a callback every duration seconds until .clear() is called. Cached.
var count = 0;
var iv = BJS.interval(0.5, function(){
count++;
console.log('tick', count);
if(count >= 5) BJS.clearInterval(iv);
});
iv.play();Load a resource via XHR. Blocks serial chains until done.
// Simple
BJS.load('/data.json', function(data){ console.log(data); }).play();
// Options object
BJS.load('/data.json', {
callback: function(data){ console.log(data); },
nocache: true,
postData: { key: 'value' }
}).play();
// In a serial chain — blocks until XHR completes
BJS.serial(
BJS.load('/data.json', function(data){ window.mydata = data; }),
BJS.to(el, { left: 500 }, 1)
).play();Fire a callback every animation frame until .clear().
var af = BJS.animationframe(function(timestamp){
el.style.transform = 'rotate(' + (timestamp * 60 % 360) + 'deg)';
});
af.play();
// Later: BJS.cancelanimationframe(af);Append a DOM child on play, remove on stop/rollback.
Remove a DOM child on play, re-append on stop/rollback.
Once you have a tween, control it with:
var tw = BJS.to(el, { left: 500 }, 2);
tw.play(); // Start or resume
tw.pause(); // Pause at current position
tw.stop(); // Stop and teardown
tw.restart(); // Seek to 0 then play
tw.seek(1); // Jump to 1 second (resets TickerListener)
tw.gotoAndPlay(0.5); // Seek to 0.5s then play
tw.gotoAndStop(0.5); // Seek to 0.5s then stop
tw.toggle(); // If playing → stop, else → play
tw.rewind(); // Seek to 0Decorators can be chained directly on a tween via prototype methods:
BJS.to(el, { left: 500 }, 2)
.reverse()
.delay(0.3)
.play();
BJS.to(el, { left: 500 }, 2)
.repeat(3)
.scale(0.5) // each repeat is now 1s
.play();Available chain methods:
| Method | Description |
|---|---|
.reverse() |
Run backwards |
.slice(begin, end, isPercent?) |
Play a portion |
.scale(factor) |
Scale duration |
.delay(delay, postDelay?) |
Add pre/post delay |
.repeat(count) |
Repeat N times (-1 = infinite) |
.then(onFulfilled) |
Callback on complete (returns this) |
BJS.pause(); // Halt ALL animation
BJS.resume(); // Resume ALL animation
BJS.isPlaying(); // Boolean — is the ticker running?
BJS.stopAll(); // Stop every active tween
BJS.clear(); // Stop all tweens, reset tickerBJS detects page visibility changes via document.hidden / visibilitychange. When the user switches tabs or minimizes the window, the ticker halts automatically and resumes when they return. No more wasted CPU on invisible animations.
For finer control, wire pause()/resume() to window focus events yourself:
// Best practice: pause on blur, resume on focus
window.addEventListener('blur', function(){ BJS.pause(); });
window.addEventListener('focus', function(){ BJS.resume(); });
// Or use page visibility for mobile-friendly detection
document.addEventListener('visibilitychange', function(){
document.hidden ? BJS.pause() : BJS.resume();
});BJS supports GSAP-style CSS transform animation with full 3D matrix decomposition and recomposition.
Wrap transforms in a transform object:
BJS.to(el, {
transform: { translateX: 200, rotate: 45 }
}, 2, Quad.easeOut).play();Supported transform properties:
| Property | Type | Example |
|---|---|---|
translateX |
Number (px) | translateX: 200 |
translateY |
Number (px) | translateY: 100 |
translateZ |
Number (px) | translateZ: 150 (needs parent perspective — see below) |
rotate / rotateZ |
Number (deg) | rotate: 45 |
rotateX |
Number (deg) | rotateX: 90 |
rotateY |
Number (deg) | rotateY: 180 |
scaleX |
Number | scaleX: 2 |
scaleY |
Number | scaleY: 0.5 |
scaleZ |
Number | scaleZ: 1.5 |
skewX |
Number (deg) | skewX: 30 |
rotate and rotateZ are aliases — animating one keeps the other in sync.
translateZ requires perspective on a parent element:
#stage {
perspective: 800px;
}BJS.to(el, { transform: { translateZ: 200, rotateY: 45 } }, 2).play();You can also animate transform as a flat CSS property with string values:
BJS.to(el, { transform: 'translateX(200px) rotate(45deg)' }, 2).play();Internally, BJS reads the current computed transform matrix via getComputedStyle, then decomposes it into individual components (translateX, rotate, scaleX, etc.) using matrix decomposition. Each component is interpolated independently, then recomposed into a CSS transform string every frame. This means you can start a transform tween on an element that already has transforms applied, and BJS correctly picks up the current state.
The PropertyMapper system is what lets BJS know how to read and write properties on different target types. It includes built-in mappers for:
| Mapper | Properties | Behavior |
|---|---|---|
TRANSFORM_reg |
transform |
CSS transform matrix decompose/interpolate/recompose |
ALL_reg |
everything else | Dash-conversion (marginLeft → margin-left), unit suffix support, relative values |
COLOR_reg |
color, backgroundColor |
Color interpolation via {r,g,b,a} objects |
ALPHA_reg |
alpha, opacity |
Cross-browser opacity with IE filter fallback |
SCROLL_reg |
scrollLeft, scrollTop |
Window/element scroll position |
Force a CSS unit by appending ::unit to the property name:
BJS.to(el, { 'width::%': 50, 'left::px': 200 }, 1).play();
BJS.to(el, { 'font-size::em': 2.5 }, 1).play();Prefix a value with $ to make it relative to the current value:
BJS.to(el, { left: '$100' }, 1).play(); // move 100px right from current
BJS.to(el, { left: '$-50' }, 1).play(); // move 50px leftAnimate colors with the Color utility. Colors are interpolated as {r,g,b,a} objects internally.
BJS.to(el, { backgroundColor: { r: 255, g: 0, b: 0 } }, 2).play();
BJS.to(el, { color: Color.toOBJ('#ff8800') }, 2).play();Color static methods:
| Method | Description |
|---|---|
Color.toOBJ(val) |
Any input → {r,g,b,a} |
Color.toSTR(val) |
Any input → 'rgb(r,g,b)' |
Color.toHEX(val) |
Any input → '#RRGGBB' |
Color.toUINT(val) |
Any input → integer |
Color.safe(val, mode?) |
Clamp to valid range |
Color.RGBtoHSV(o) |
{r,g,b,a} → {h,s,v,a} |
Color.HSVtoRGB(o) |
{h,s,v,a} → {r,g,b,a} |
Color.HSLtoRGB(o) |
{h,s,l,a} → {r,g,b,a} |
Color.RGBtoHSL(o) |
{r,g,b,a} → {h,s,l,a} |
Color.HSVtoHSL(o) |
{h,s,v,a} → {h,s,l,a} |
Color.HSLtoHSV(o) |
{h,s,l,a} → {h,s,v,a} |
var red = Color.toOBJ('#ff0000'); // { r:255, g:0, b:0, a:1 }
var rgb = Color.toSTR(red); // 'rgb(255,0,0)'
var hex = Color.toHEX(red); // '#ff0000'BJS has two ticker layers:
AnimationTicker— the low-levelrequestAnimationFramemanager. Handles frame timing, FPS calculation, and the global render loop.EnterFrameTicker— manages the linked-list of active tween listeners. Each playing tween registers aTickerListenerthat receivestick(time)per frame.
You generally don't interact with the ticker directly, but these methods are available:
AnimationTicker.start(); // Begin the rAF loop (auto-started on first tween)
AnimationTicker.stop(); // Stop the rAF loop
AnimationTicker.haltSystem(); // Emergency halt
AnimationTicker.restoreSystem(); // Restart after haltThe ticker uses a padding sentinel linked list technique. When a tween completes and removes itself during iteration, the ticker safely continues via padding fallback nodes.
var el = document.getElementById('box');
// Move smoothly
BJS.to(el, { left: 500 }, 1.5, Quad.easeOut).play();
// Fade out
BJS.to(el, { opacity: 0 }, 1, Cubic.easeIn).play();
// Animate multiple properties at once
BJS.to(el, {
left: 500,
top: 300,
width: 200,
height: 200,
opacity: 0.8
}, 2, Quart.easeOut).play();var el = document.getElementById('box');
BJS.serial(
BJS.to(el, { left: 500 }, 1, Quad.easeOut),
BJS.to(el, { top: 300, backgroundColor: { r: 255, g: 0, b: 0 } }, 1, Quad.easeOut),
BJS.to(el, { left: 100, top: 100 }, 1, Bounce.easeOut)
).play();var el1 = document.getElementById('box1');
var el2 = document.getElementById('box2');
BJS.parallel(
BJS.to(el1, { left: 500 }, 2, Quad.easeOut),
BJS.to(el2, { top: 400 }, 1.5, Cubic.easeInOut)
).play();
// Or with an array target (auto-parallel)
BJS.create({
target: [el1, el2],
to: { opacity: 0 },
time: 1.5
}).play();var items = document.querySelectorAll('.item');
BJS.stagger(items, { left: 500 }, {
time: 0.5,
staggerDelay: 0.1,
ease: Back.easeOut
}).play();var el = document.getElementById('card');
BJS.to(el, {
transform: { translateX: 300, rotate: 180, scaleX: 1.5 }
}, 2, Elastic.easeOut).play();#stage { perspective: 800px; }var el = document.getElementById('card');
BJS.parallel(
BJS.to(el, { transform: { translateZ: 200, rotateY: 90 } }, 2, Quad.easeOut),
BJS.to(el, { opacity: 0.5 }, 2)
).play();BJS.to(document.getElementById('box'), { left: 500 }, 2)
.reverse()
.delay(0.5)
.repeat(2)
.play();var tl = BJS.timeline();
tl.add(BJS.to(el, { left: 300 }, 1));
tl.add(BJS.to(el, { top: 200 }, 1));
tl.add(function(){ console.log('done!'); });
tl.play();
// With delays between
var tl2 = BJS.timeline();
tl2.add(BJS.to(el, { left: 300 }, 1), 0.5); // 0.5s gap
tl2.add(BJS.to(el, { top: 200 }, 1));
tl2.play();// Load data, then animate, then log
BJS.serial(
BJS.load('/api/data.json', function(data){ window.data = data; }),
BJS.to(el, { left: 500 }, 1, Quad.easeOut),
BJS.func(function(){ console.log('animation done, data:', window.data); })
).play();
// Wait 2 seconds, then animate, then wait 1s, then animate back
BJS.serial(
BJS.timeout(2, function(){ console.log('starting...'); }),
BJS.to(el, { left: 500 }, 1, Bounce.easeOut),
BJS.timeout(1),
BJS.to(el, { left: 100 }, 1)
).play();var count = 0;
var iv = BJS.interval(0.3, function(){
count++;
el.style.left = (count * 50) + 'px';
if(count >= 10) BJS.clearInterval(iv);
});
iv.play();// Constant velocity — takes exactly as long as needed to travel 500px at 200px/frame
BJS.create({
target: el,
to: { left: 500 },
from: { left: 0 },
ease: Physical.uniform(200, 60)
}).play();
// Accelerating
BJS.create({
target: el,
to: { left: 500 },
ease: Physical.accelerate(30, 0, 60)
}).play();BJS.to(el, {
backgroundColor: { r: 255, g: 0, b: 0 },
color: { r: 255, g: 255, b: 255 }
}, 2, Quad.easeOut).play();var tw = BJS.to(el, { left: 500 }, 2, Bounce.easeOut);
document.getElementById('go-btn').onclick = function(){ tw.restart(); };
document.getElementById('pause-btn').onclick = function(){ tw.pause(); };// Single-page app navigation
function navigate(url) {
BJS.stopAll(); // kill all running animations
// ... load new page ...
}BJS.bezierTo(
el,
{ left: 800, top: 400 },
{ left: [300, 500, 600], top: [50, 300, 100] },
3,
Quad.easeInOut
).play();A 100px square exits the right edge of the window and re-enters from the left — a seamless horizontal loop. The trick: make the outbound tween wait half-way, then the inbound tween covers the rest, giving the illusion of one continuous scroll.
var el = document.getElementById('box');
var winW = window.innerWidth;
var size = 100;
// Exit right (0 → winW), then arrive from left (-size → 0)
BJS.serial(
BJS.to(el, { left: winW, /* */ onComplete: function(){
el.style.left = -size + 'px'; // instantly reposition off-screen left
}}, 1.5, Quad.easeIn),
BJS.to(el, { left: 0 /* */ }, 1.5, Quad.easeOut)
).repeat(-1).play();The .repeat(-1) makes it loop forever. The onComplete callback snaps the element off-screen left between the two tweens — invisible because the element is outside the viewport.
No manual onComplete hack needed. Create one long tween that overshoots both sides, then slice it in half and reorder the slices:
var el = document.getElementById('box');
var size = 100;
// Single tween: off-screen left → off-screen right
// (use .tween() with an explicit from, or .create())
var tw = BJS.tween(el, { left: window.innerWidth }, { left: -size }, 3, Linear.easeIn);
// Slice at midpoint, reorder: tail first, head second
BJS.serial(
tw.slice(0.5, 1), // second half: off-screen-left → center
tw.slice(0, 0.5) // first half: center → off-screen-right
).repeat(-1).play();This works because slice(0.5, 1) plays the tail of the original timeline (from the center to the right edge), then slice(0, 0.5) plays the head (from the left edge to the center). The element loops without ever needing a manual reposition — the from value (-size, off-screen left) is the starting position on first play, and slice just selects the correct portions of the path.
| Method | Returns | Description |
|---|---|---|
create(opts) |
TweenLike |
Universal factory |
tween(tg, to, from, time, ease) |
Tween |
Positional tween |
to(tg, to, time, ease) |
Tween |
Target to values |
from(tg, from, time, ease) |
Tween |
From values to current |
apply(opts) |
Tween |
Instant apply (one frame) |
instant(tg, props) |
— | Immediate set |
bezier(tg, to, from, cp, time, ease) |
Tween |
Bezier path |
bezierTo(tg, to, cp, time, ease) |
Tween |
Bezier (auto-source) |
bezierFrom(tg, from, cp, time, ease) |
Tween |
Bezier (auto-target) |
physical(tg, to, from, ease) |
Tween |
Physical ease tween |
physicalTo(tg, to, ease) |
Tween |
Physical (auto-source) |
physicalFrom(tg, from, ease) |
Tween |
Physical (auto-target) |
physicalApply(tg, to, from, ease, t) |
Tween |
Physical instant apply |
serial(...tweens) |
SerialTween |
Sequential composition |
serialTweens(array) |
SerialTween |
Sequential from array |
parallel(...tweens) |
ParallelTween |
Parallel composition |
parallelTweens(array) |
ParallelTween |
Parallel from array |
reverse(tween) |
ReversedTween |
Decorator: reverse |
slice(tween, begin, end, isPercent?) |
SlicedTween |
Decorator: slice |
scale(tween, factor) |
ScaledTween |
Decorator: scale time |
delay(tween, delay, postDelay?) |
DelayedTween |
Decorator: delay |
repeat(tween, count?) |
RepeatedTween |
Decorator: repeat |
stagger(targets, to, opts) |
ParallelTween |
Staggered animation |
timeline() |
TimelineBuilder |
Fluent timeline |
func(fn, params?, rollback?, ...) |
FunctionAction |
Action: execute func |
timeout(dur, fn, params?) |
TimeoutAction |
Action: delayed callback |
interval(dur, fn, params?) |
IntervalAction |
Action: repeating callback |
load(url, cb|opts, params?) |
LoadAction |
Action: XHR load |
animationframe(fn, params?) |
AnimationFrameAction |
Action: per-frame callback |
addChild(tg, parent) |
AddChildAction |
Action: append DOM |
removeFromParent(tg) |
RemoveFromParentAction |
Action: remove DOM |
clearTimeout(uid) |
— | Cancel timeout |
clearInterval(uid) |
— | Cancel interval |
clearLoad(uid) |
— | Cancel load |
cancelanimationframe(uid) |
— | Cancel anim frame |
pause() |
— | Halt all animation |
resume() |
— | Resume all animation |
isPlaying() |
Boolean |
Check if ticker is running |
clean() |
— | Stop all, reset ticker |
stopAll() |
— | Stop every active tween |
restart(tween) |
— | Stop → seek(0) → play |
| Method | Description |
|---|---|
play() |
Start or resume |
stop() |
Stop and teardown |
restart() |
Seek(0) → play |
pause() |
Pause at current position |
toggle() |
Play ↔ stop |
start() |
Rewind → play |
seek(pos, isPercent?) |
Jump to position |
gotoAndPlay(pos, isPercent?) |
Seek → play |
gotoAndStop(pos, isPercent?) |
Seek → stop |
rewind() |
Seek to 0 |
reverse() |
Return ReversedTwen (fluent) |
slice(begin, end, isPercent?) |
Alt SlicedTween (fluent) |
scale(factor) |
Return ScaledTween (fluent) |
delay(delay, postDelay?) |
Return DelayedTween (fluent) |
repeat(count) |
Return RepeatedTween (fluent) |
then(onFulfilled) |
Register onComplete callback |
configure(opts) |
Set options |
bind(type, fn) |
Register event handler |
unbind(type, fn) |
Unregister event handler |
fire(type) |
Fire event |
clone() |
Deep copy |
destroy() |
Clean up |
| Expression | Value |
|---|---|
Linear.easeIn |
Linear function |
Linear.easeOut |
= Linear.easeIn (identity) |
Quad.easeIn |
Quadratic acceleration |
Quad.easeOut |
Quadratic deceleration |
Quad.easeInOut |
Both (S-curve) |
Cubic.easeIn |
Cubic acceleration |
Back.easeInWith(s) |
Back with custom overshoot |
Elastic.easeOutWith(a, p) |
Elastic with custom amplitude/period |
Physical.uniform(v, fps) |
Constant velocity |
Physical.accelerate(a, iv, fps) |
Constant acceleration |
Physical.exponential(f, t, fps) |
Exponential growth/decay |
Custom.func(fn) |
Custom ease function |
| Event | Fires When |
|---|---|
onStart |
Tween begins |
onPlay |
Each time tween plays (including re-plays) |
onUpdate |
Every frame (during animation) |
onDraw |
Every frame (after property update) |
onStop |
Tween stopped manually |
onComplete |
Tween reaches end |
BJS.create({
target: el,
to: { left: 500 },
time: 2,
onStart: function(){ console.log('start'); },
onUpdate: function(){ console.log('updating...'); },
onComplete: function(){ console.log('complete!'); }
}).play();BetweenJS is one file. No npm install, no bundler config, no build step. Drop it in a <script> tag and you have:
- 11 ease families with
easeIn/easeOut/easeInOut/easeOutIn— plus physical eases that compute their own duration - 3D CSS transforms with full matrix decomposition — the only thing lighter than GSAP that does this
- Composable tweens —
serial(),parallel(), decorators (reverse,slice,scale,delay,repeat), all chainable - Action tweens —
timeout(),interval(),load(),func(),animationframe()— that block serial chains for real sequencing - No dependencies. ~25 KB gzipped. ES5 — runs everywhere.
That's it. Open your console and try it now:
BJS.to(document.body, { backgroundColor: { r: 30, g: 30, b: 50 } }, 1).play();Happy tweening 🚀