A generator-based tween function for animating values over time with cubic bezier easing.
import { tween, easings } from 'remix/component'
let animation = tween({
from: 0,
to: 100,
duration: 1000,
curve: easings.easeInOut,
})
// Initialize generator
animation.next()
function animate(timestamp: number) {
let { value, done } = animation.next(timestamp)
element.style.transform = `translateX(${value}px)`
if (!done) requestAnimationFrame(animate)
}
requestAnimationFrame(animate)The tween function returns a generator that:
- Yields the current interpolated value on each iteration
- Receives the current timestamp via
next(timestamp) - Returns
done: truewhen the duration has elapsed
The generator uses cubic bezier curves to map linear time progress to eased value progress, matching CSS cubic-bezier() timing functions.
Built-in easing curves matching CSS timing functions:
import { easings } from 'remix/component'
easings.linear // { x1: 0, y1: 0, x2: 1, y2: 1 }
easings.ease // { x1: 0.25, y1: 0.1, x2: 0.25, y2: 1 }
easings.easeIn // { x1: 0.42, y1: 0, x2: 1, y2: 1 }
easings.easeOut // { x1: 0, y1: 0, x2: 0.58, y2: 1 }
easings.easeInOut // { x1: 0.42, y1: 0, x2: 0.58, y2: 1 }Define custom bezier curves with control points:
let customCurve = {
x1: 0.68,
y1: -0.55,
x2: 0.265,
y2: 1.55,
}
let animation = tween({
from: 0,
to: 100,
duration: 500,
curve: customCurve,
})The control points match CSS cubic-bezier(x1, y1, x2, y2) syntax.
Use tween with handle.signal for automatic cleanup:
function AnimatedValue(handle: Handle) {
let value = 0
function animateTo(target: number) {
let animation = tween({
from: value,
to: target,
duration: 300,
curve: easings.easeOut,
})
animation.next() // Initialize
function tick(timestamp: number) {
if (handle.signal.aborted) return
let result = animation.next(timestamp)
value = result.value
handle.update()
if (!result.done) {
requestAnimationFrame(tick)
}
}
requestAnimationFrame(tick)
}
return () => (
<div>
<div style={{ transform: `translateX(${value}px)` }}>Moving</div>
<button
mix={[
pressEvents(),
on('press', () => {
animateTo(200)
}),
]}
>
Animate
</button>
</div>
)
}Animate multiple values with separate tweens:
let xAnimation = tween({ from: 0, to: 100, duration: 500, curve: easings.easeOut })
let yAnimation = tween({ from: 0, to: 50, duration: 500, curve: easings.easeOut })
let scaleAnimation = tween({ from: 1, to: 1.5, duration: 500, curve: easings.easeOut })
xAnimation.next()
yAnimation.next()
scaleAnimation.next()
function animate(timestamp: number) {
let x = xAnimation.next(timestamp)
let y = yAnimation.next(timestamp)
let scale = scaleAnimation.next(timestamp)
element.style.transform = `translate(${x.value}px, ${y.value}px) scale(${scale.value})`
if (!x.done || !y.done || !scale.done) {
requestAnimationFrame(animate)
}
}
requestAnimationFrame(animate)Creates a generator that interpolates between values over time.
interface TweenOptions {
from: number // Starting value
to: number // Ending value
duration: number // Duration in milliseconds
curve: BezierCurve // Easing curve
}
interface BezierCurve {
x1: number // First control point X (0-1)
y1: number // First control point Y
x2: number // Second control point X (0-1)
y2: number // Second control point Y
}Returns: Generator<number, number, number> - Yields current value, returns final value when done.
Object containing preset bezier curves:
| Preset | Description |
|---|---|
linear |
No easing, constant speed |
ease |
Default CSS ease |
easeIn |
Slow start, fast end |
easeOut |
Fast start, slow end |
easeInOut |
Slow start and end |
Use tween for:
- Imperative animations driven by
requestAnimationFrame - Canvas/WebGL animations
- Animating non-CSS properties
- Complex sequenced animations
For most UI animations, prefer animation mixins (animateEntrance, animateExit, animateLayout)
or CSS transitions with spring.
- Spring API - Physics-based easing for CSS