# wobble animates one number with the same equations as CASpringAnimation

> wobble is a MIT licensed TypeScript library of about 1.7 KB gzipped that animates a scalar value using a closed-form damped harmonic oscillator, the same approach Apple takes in CASpringAnimation. It deliberately stops at one dimension and one value, leaving chaining and multi-dimensional springs to you.

**skevy/wobble** — A tiny (~1.7 KB gzipped) spring physics micro-library that models a damped harmonic oscillator.

- Repository: https://github.com/skevy/wobble
- Website: https://wobble-demos.now.sh
- Stars: 865 · Forks: 18
- Language: TypeScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/skevy-wobble

## One scalar, one spring, nothing else

The scope statement is the first thing in the README and it is unusually blunt. wobble intentionally only provides a way to animate a scalar value according to the equations governing damped harmonic motion, and the documentation says that is all the library will ever do. Anything else, and the list is specific, such as integration with a UI library, multi-dimensional springs, or a nice API around chaining springs together, is left to the reader to implement. The Why Would I Use This section answers with a joke about dancing, and then gets to the real reason: you need a very small and accurate damped harmonic spring simulation inside an animation library or application. That is a narrow product, and the narrowness is a feature for anyone who has been forced to adopt a general purpose animation engine to move one number from 0 to 100.

## The closed-form solution is why it can be interrupted

Two approaches dominate spring animation. One is numerical integration, commonly Runge-Kutta 4th order, which approximates the exact solution, is generally easier to derive, and applies to almost any ordinary differential equation, at the cost of being slower. The other is the closed-form exact solution. The project uses the second, and explains why that matters in three ways: it is easier to generate keyframes and to build continuous or interruptible gestures and animations, because position and velocity are pure functions of time; it needs less code; and it is faster. The origin story is specific. The goal was to replicate CASpringAnimation from Apple's Core Animation library, used to power macOS and iOS animations, in order to mimic iOS animations in React Native. After some spelunking inside QuartzCore.framework, the author concluded Apple was using the closed-form solution, and wobble was built on the same equations so it could match Apple animations precisely. The mathematics is presented as the damped harmonic oscillator ODE, its general solution, and the derived cases for under-damped, critically-damped and over-damped motion, with a full proof linked from planetmath.org.

## Installing wobble and driving a spring from listeners

The package installs the usual way:

```bash
yarn add wobble
# or
npm install --save wobble
```

The usage example in the README is the whole mental model of the library. You construct a Spring with a configuration, attach listeners for the start, update and stop events, and then start it:

```js
import { Spring } from 'wobble';

// Create a new spring
const spring = new Spring({
  toValue: 100,
  stiffness: 1000,
  damping: 500,
  mass: 3,
});

// Set listeners for spring events, start the spring.
spring
  .onStart(() => {
    console.log('Spring started!');
  })
  .onUpdate((s) => {
    console.log(`Spring's current value: ` + s.currentValue);
    console.log(`Spring's current velocity: ` + s.currentVelocity);
  })
  .onStop(() => {
    console.log('Spring is at rest!');
  })
  .start();
```

Every listener registration returns the Spring, so the calls chain. The onUpdate callback receives the spring itself and reads currentValue and currentVelocity, which is how you feed a render loop. Note the units of velocity in the configuration section, which are in units per millisecond, since that detail decides whether a threshold you set means what you intended.

## Configuration defaults that define the feel of the motion

The configuration surface is a short list of numbers with defaults, and it maps closely onto what a designer expects from a spring. fromValue starts the animation and defaults to 0, toValue ends it and defaults to 1. stiffness is the spring stiffness coefficient and defaults to 100. damping defines how the motion is damped by friction forces and defaults to 10. mass is the mass of the object attached to the end of the spring and defaults to 1, while initialVelocity is the starting velocity in units per millisecond and defaults to 0. Three of the settings are about behaviour under extremes. allowsOverdamping controls whether a damping ratio above 1 is permitted and defaults to false. overshootClamping is false when overshooting is allowed and true when it is not, also defaulting to false. Two thresholds decide when the spring is done: restVelocityThreshold, below which the velocity means the spring is at rest, and restDisplacementThreshold, below which the displacement means the same. Both default to .001, and since the example uses a toValue of 100, a threshold tuned for a 0 to 1 animation may stop early or run long on a larger range.

## Starting, stopping and retargeting a running spring

The method list is short, and two methods carry most of the weight. start() begins the simulation and calls the onStart listeners, and the documentation is precise about when it does anything: if fromValue differs from toValue, or if initialVelocity is non-zero. A spring that is already at its target with no initial velocity does not animate. stop() ends a simulation in progress and fires the onStop listeners. The third is updateConfig(updatedConfig), which takes a partial configuration and applies the supplied values while reusing any that were not explicitly supplied. That is the interruption story. Because position and velocity are pure functions of time, a running spring can be retargeted by updating its configuration and continuing from the current state, which is exactly what makes continuous and interruptible gestures practical. Listeners can be attached at any point with onStart, onUpdate and onStop, and removeListener exists for detaching one. The listener signature is a single SpringListenerFn type shared across the three events.

## Four bundles, a jest suite and a hand-rolled rAF shim

The build is unusually well specified for a library this small, and the package.json shows it. TypeScript sources are compiled with tsc, and rollup produces three additional bundles, an ES module, a CommonJS build and a browser build:

```json
    "build": "tsc",
    "dist-es": "rollup -c --environment ROLLUP_CONFIG:es",
    "dist-cjs": "rollup -c --environment ROLLUP_CONFIG:cjs",
    "dist-browser": "rollup -c --environment ROLLUP_CONFIG:browser",
```

The published entry points are split to match, with main pointing at the CommonJS module, module at the ES bundle, types at the generated declarations and typescript:main at the raw source. The test script is tsc --noEmit followed by jest --coverage, and coverage is uploaded to codecov by a posttest hook. The jest configuration is where the animation detail shows up:

```json
    "setupFiles": [
      "./jest/rAF.js"
    ],
```

A requestAnimationFrame shim is installed as a setup file, and the jest timers setting is fake, which is what makes a time-based simulation testable at all. Linting uses tslint with a codeFrame formatter, the repository also carries rollup.config.js, tsconfig.json and tslint.json, and the demos live in their own directory with a dev script that runs the watcher and the demo server together.

## Where wobble stops and what you use instead

The boundary is the library's own stated scope. There is no multi-dimensional spring, so a spring that moves in x and y means two spring instances and your own composition. There is no chaining API, so a spring that follows another spring means reading the first spring's current value and feeding it into the second. There is no UI library integration, so connecting a spring to a React Native animated value or a DOM style is code you write. The alternative approaches are named in the README and differ in exactly one way, the solver. Rebound and Pop, both from the Facebook projects, use RK4 numerical integration, which approximates the solution, is slower, and makes interruptible and continuous gestures harder to express. Apple's own CASpringAnimation is the other option, and it is available directly on iOS and macOS without a JavaScript dependency at all, though it does not exist on the web or on Android. wobble is the choice when you want the closed-form behaviour outside Apple's platforms, and the price of that choice is that the surrounding animation plumbing is yours to build.

## Conclusion

wobble suits a React Native or web animation codebase that needs one spring per animated value, values that can be retargeted mid-flight, and a dependency measured in kilobytes rather than megabytes. It is the wrong tool for multi-dimensional springs, chains of springs, or an integration with a specific UI library, since the project states outright that all of that is left to the reader. Verify first that the closed-form result matches the platform you are imitating, because matching CASpringAnimation was the original goal rather than a documented guarantee, and note that the npm version is 1.5.1 while the only tagged GitHub release in the list is v0.1.0 from 2017.

## FAQ

### How do I install wobble?

Run yarn add wobble, or npm install --save wobble. The package is published as wobble on npm and the current npm version is 1.5.1.

### What does wobble animate?

Only a scalar value, using the equations of a damped harmonic oscillator. The README states that integration with a UI library, multi-dimensional springs and chaining springs are all left to the reader to implement.

### How does wobble differ from Rebound or Pop?

Rebound and Pop use RK4 numerical integration, which approximates the exact solution and is slower. wobble uses the closed-form solution, so position and velocity are pure functions of time, which makes keyframes and interruptible gestures easier and the code smaller.

### What are the default spring configuration values in wobble?

fromValue defaults to 0, toValue to 1, stiffness to 100, damping to 10, mass to 1 and initialVelocity to 0. allowsOverdamping and overshootClamping default to false, and both restVelocityThreshold and restDisplacementThreshold default to .001.

### Can a running wobble spring be retargeted?

Yes, through updateConfig, which takes a partial configuration and reuses any values not supplied. Because the closed-form solution makes position and velocity pure functions of time, a spring can be interrupted and continued from its current state.

## Sources

- [License: MIT](https://github.com/skevy/wobble/blob/develop/LICENSE)
- [Project website](https://wobble-demos.now.sh)
- [README](https://github.com/skevy/wobble/blob/develop/README.md)
- [Releases](https://github.com/skevy/wobble/releases)
- [skevy/wobble on GitHub](https://github.com/skevy/wobble)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/skevy-wobble
