npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tweens/tweens

v0.3.0

Published

Physics-accurate spring animation engine for the DOM. Analytical solver, interruptible, Promise-based.

Readme

tweens

Physics-accurate spring animation engine for the DOM. Analytical solver — no numerical drift, no fixed durations, interruptible at any point.

tweens.dev · npm install @tweens/tweens


Quick start

import { spring } from 'tweens'

const el = document.getElementById('card')

// Move right 100px with a bouncy spring
await spring(el, { x: 100 }, 'bouncy')

// Scale and fade in one call
await spring(el, { scale: 1.1, opacity: 0.5 }, 'snappy')

// Custom physics
spring(el, { x: 0 }, { stiffness: 300, damping: 20 })

// Design by feel — no physics required
spring(el, { y: -40 }, { duration: 0.4, bounce: 0.3 })

API

spring(el, props, options?)

Animates one or more properties to target values. Returns a Promise<void> that resolves when the spring settles.

spring(
  el: HTMLElement,
  props: SpringProps,
  options?: SpringConfig | SpringPreset,
): Promise<void>

Multiple props animate in parallel and the promise resolves when all settle.

// Sequence two animations
await spring(el, { x: 100 })
await spring(el, { y: 50, scale: 1.2 })

enter(el, fromProps, options?)

Animates FROM values back to where the element currently is. Useful for entrance animations.

enter(
  el: HTMLElement,
  fromProps: SpringProps,
  options?: SpringConfig | SpringPreset,
): Promise<void>
import { enter } from 'tweens'

// Element slides and fades in from below
enter(el, { y: 24, opacity: 0 }, 'snappy')

tween(el, fromProps, toProps, options?)

Animates FROM one set of values TO another.

tween(
  el: HTMLElement,
  fromProps: SpringProps,
  toProps: SpringProps,
  options?: SpringConfig | SpringPreset,
): Promise<void>
import { tween } from 'tweens'

tween(el, { x: -200, opacity: 0 }, { x: 0, opacity: 1 }, 'gentle')

cascade(els, props, options?, interval?)

Animates a list of elements with a time offset between each. Returns a Promise<void> that resolves when all elements settle.

cascade(
  els: NodeListOf<Element> | HTMLElement[],
  props: SpringProps,
  options?: SpringConfig | SpringPreset,
  interval?: number,  // seconds between each element, default 0.05
): Promise<void>
import { cascade } from 'tweens'

const items = document.querySelectorAll('.list-item')
cascade(items, { y: 0, opacity: 1 }, 'snappy', 0.06)

snap(el, props)

Instantly sets properties with no animation. Cancels any running animation for those props.

snap(
  el: HTMLElement,
  props: SpringProps,
): void
import { snap } from 'tweens'

snap(el, { x: 0, opacity: 1 })

stop(el, prop?)

Cancels animations on an element. Pass a prop to cancel just that property, or omit to cancel all.

stop(
  el: HTMLElement,
  prop?: AnimatableProp,
): void
import { stop } from 'tweens'

stop(el)         // cancel everything
stop(el, 'x')    // cancel only x

SpringProps

Any combination of these properties:

| Prop | Unit | Default | | --------- | ------- | ------- | | x | px | 0 | | y | px | 0 | | scale | – | 1 | | rotate | degrees | 0 | | opacity | 0–1 | 1 | | blur | px | 0 |

Multiple transform props compose correctly — spring(el, { x: 100 }) followed by spring(el, { scale: 1.2 }) won't clobber each other.


SpringConfig

Two ways to configure a spring:

Physics params

spring(el, { x: 100 }, {
  stiffness: 300,  // spring constant  (default: 170)
  damping:   20,   // friction         (default: 26)
  mass:      1,    // inertia          (default: 1)
})

The damping ratio ζ = damping / (2 * √(stiffness * mass)) determines the character:

  • ζ < 1 — underdamped, overshoots and oscillates
  • ζ = 1 — critically damped, fastest settle with zero overshoot
  • ζ > 1 — overdamped, slow creep toward target

Duration + bounce

More intuitive — design by feel without thinking about physics:

spring(el, { x: 100 }, {
  duration: 0.4,  // seconds until effectively settled
  bounce:   0.3,  // 0 = no overshoot, 1 = maximum oscillation
})

Orchestration

spring(el, props, {
  delay:      0.2,   // seconds before animation starts
  repeat:     3,     // extra plays after the first; -1 = infinite
  yoyo:       true,  // reverse direction on every other repeat
  onStart:    () => {},
  onUpdate:   (value) => {},  // fires every frame
  onComplete: () => {},
})

Relative values

spring(el, { x: '+=100' })  // add 100 to current x
spring(el, { x: '-=50'  })  // subtract 50 from current x

SpringPreset

| Preset | ζ | Character | | --------- | ----- | -------------------------------------- | | bouncy | 0.612 | Lively, ~9% overshoot | | snappy | 0.850 | Decisive, <1% overshoot | | gentle | 0.783 | Slow and smooth | | stiff | 1.02 | Overdamped — snaps to target instantly |

spring(el, { scale: 1.1 }, 'snappy')

Interruptibility

If you call spring() on an element that's already animating, the new spring picks up the current position and velocity — no snap, no pop:

// User hovers → scale up
el.addEventListener('mouseenter', () => spring(el, { scale: 1.05 }, 'snappy'))

// User leaves mid-animation → continues from wherever it is
el.addEventListener('mouseleave', () => spring(el, { scale: 1 }, 'snappy'))

Accessibility

Respects prefers-reduced-motion automatically. When the user has reduced motion enabled, spring() writes the target value instantly and resolves the Promise — no rAF loop, no animation.


How it works

The solver uses the closed-form solution to the damped harmonic oscillator (mx'' + bx' + kx = 0). At every animation frame it calculates exact position and velocity from the analytical formula — not Euler stepping. This means:

  • No accumulated numerical error over long animations
  • Exact velocity is available at any time for interruptibility
  • No fixed duration — the spring runs until it physically settles

Three cases are handled: underdamped (oscillates), critically damped (fastest settle), and overdamped (slow creep).


Performance

Only transform and opacity (and filter for blur) are animated — the GPU-composited properties. will-change is set automatically on animation start and cleared when all springs on the element settle.