Skip to content
Updated April 2026Edit this page ↗

Springs

Spring animations in TermUI are physics-based. Instead of specifying a duration and easing curve, you describe how the spring feels: stiffness, damping, mass.

The math handles the rest. The result is motion that looks natural, with overshoot and settling that you don't have to hand-tune.

Installation

TYPESCRIPT
npm install @termuijs/motion

When to use springs

Springs shine in three scenarios:

  • Interrupted animations: if a value is mid-flight when the target changes, a spring naturally continues from its current velocity.

Easing functions reset abruptly.

  • Physical UI: panels that drag open, items that snap into place, progress bars that overshoot then correct.

The motion reads as intentional rather than programmed.

  • Chained motion: one spring's output feeds another's input, producing cascaded motion without manual orchestration. For fixed-duration, one-shot transitions (page reveals, fade-ins), easings are simpler and more predictable.

Basic usage

Springs work on a state machine. Each call to stepSpring() advances the simulation by one time step:

TYPESCRIPT
import { stepSpring, SPRING_PRESETS } from '@termuijs/motion'
import type { SpringState } from '@termuijs/motion'
 
let state: SpringState = {
    value: 0,
    velocity: 0,
    target: 100,
    done: false,
}
 
// Advance one frame (dt in seconds, so 1/60 ≈ 16ms)
state = stepSpring(state, SPRING_PRESETS.default, 1 / 60)
console.log(state.value)  // → moving toward 100
console.log(state.done)   // → false (still animating)

animateSpring helper

If you don't want to manage the loop yourself, animateSpring() runs the full animation and calls you on each frame:

TYPESCRIPT
import { animateSpring, SPRING_PRESETS } from '@termuijs/motion'
 
animateSpring({
    from: 0,
    to: 40,
    config: SPRING_PRESETS.wobbly,
    onUpdate: (value) => {
screen.writeString(Math.round(value), 5, '●')
    },
    onComplete: () => {
console.log('settled')
    },
})

Presets

PresetStiffnessDampingHow it feels
default17026Balanced, general purpose
gentle12014Soft and eased
wobbly18012Bouncy, overshoots the target
stiff21020Snappy, settles fast
slow28060Deliberate, smooth ramp
molasses280120Very slow, heavy feel

Custom springs

Pass your own config instead of a preset. Higher stiffness = faster.

Higher damping = less bounce.

TYPESCRIPT
state = stepSpring(state, {
    stiffness: 300,
    damping: 10,
    mass: 2,
}, 1 / 60)

Integration example

A progress bar that animates to real CPU load values using a spring:

TYPESCRIPT
import { App } from '@termuijs/core'
import { Box, Text, ProgressBar } from '@termuijs/widgets'
import { animateSpring, SPRING_PRESETS } from '@termuijs/motion'
import { cpu } from '@termuijs/data'
 
const bar = new ProgressBar({ width: 30, label: 'CPU' })
let currentValue = 0
 
async function updateCpu() {
    const info = await cpu()
    const target = info.total / 100
 
    animateSpring({
from: currentValue,
to: target,
config: SPRING_PRESETS.stiff,
onUpdate: (v) => {
currentValue = v
bar.setValue(Math.max(0, Math.min(1, v)))
app.requestRender()
},
    })
}
 
const root = new Box({ padding: 1 })
root.addChild(new Text('System load', { bold: true }))
root.addChild(bar)
 
const app = new App(root, { fullscreen: true })
await app.mount()
 
setInterval(updateCpu, 1000)

See also