Spring motion

Updated: Sep 24, 2026
Use a spring when a target can change while animation is running. A spring moves toward a target rather than along a fixed timeline, so retargeting continues from the current position and velocity and repeated updates remain continuous.
Use a spring when the animated value is continuous and may be interrupted — a live measurement, a value that follows input, a control that should settle physically after a release. Use interpolated motion when the change is a discrete A-to-B with a known duration.
useSpringAnimation drives values imperatively. It never re-renders the calling component per frame; you receive each value in a callback and write it straight into the DOM.

Usage

import {useEffect, useRef} from 'react';
import {SpringConfigs, useSpringAnimation} from '@wearables-ui-toolkit/mrbd';

export function CompassNeedle({heading}: {heading: number}) {
  const needleRef = useRef<HTMLDivElement>(null);

  const spring = useSpringAnimation({
    config: SpringConfigs.SOFT,
    initialValue: heading,
    onChange: value => {
      needleRef.current?.style.setProperty('--compass-heading', String(value));
    },
  });

  useEffect(() => {
    spring.setTarget(heading);
  }, [heading, spring]);

  return <div ref={needleRef} className="compass-needle" aria-hidden="true" />;
}
.compass-needle {
  inline-size: var(--uit-spacing-xsmall);
  block-size: var(--uit-spacing-3xl);
  border-radius: var(--uit-corner-radius-full);
  background: var(--uit-color-icon-primary);
  transform-origin: bottom center;
  rotate: calc(var(--compass-heading, 0) * 1deg);
}
heading can change every time a new reading arrives. Each setTarget reuses the spring’s current position and velocity, so the needle never jumps and never restarts.

API

useSpringAnimation(options)

Creates a spring controller that is stable for the lifetime of the component. Changing the config, the frame rate, or the callbacks reconfigures the existing controller in place — it does not create a new one and does not restart an animation that is already running. Because the controller identity never changes, it is safe to list in effect dependency arrays.
The hook calls onChange once with the current value when it mounts, so the element you are driving is correct on first paint.

UseSpringAnimationOptions

FieldTypeDefaultDescription
config
SpringConfig
—
Spring physics. Required. Validated when the controller is created and again whenever the values change.
initialValue
number
—
Starting value. Required, and must be finite. Read only when the controller is created; to move the spring later, call setTarget.
onChange
(value: number) => void
—
Required. Receives the initial value and every subsequent animated value. An imperative hot path: write to the DOM, a CSS custom property, a canvas, or another imperative sink. Calling a React state setter here reintroduces a render per frame.
onRest
(value: number) => void
—
Called once after the spring reaches its target or snaps to it. A snap is silent when the value is already at the target.
frameRate
number
30
Maximum number of value updates per second. Must be finite and positive.
The callbacks are read fresh on every render, so passing inline closures is fine and they always see current props.

SpringAnimationController

MemberSignatureDescription
getValue
() => number
The most recently emitted value.
isAnimating
() => boolean
Whether a frame is currently scheduled. false once the spring has settled, and while it is paused or stopped.
setTarget
(targetValue: number) => void
Retargets the spring, preserving its current velocity. Clears a previous stop(). Throws if the value is not finite.
stop
() => void
Cancels scheduled frames, holds the current value, and clears velocity. The value stays held — including through config changes — until the next setTarget.

SpringConfig

FieldTypeDescription
stiffness
number
Strength of the pull toward the target. Must be positive. Higher covers the distance faster.
damping
number
Resistance to motion. Must be zero or greater. Lower relative to stiffness means more overshoot.
mass
number
Inertia of the animated value. Must be positive. Higher makes the response heavier and slower.
SpringConfig also declares one optional field, equilibriumMode, which tunes how aggressively the spring is treated as settled. The four general-purpose presets below set it to its most aggressive value: fixed velocity and position thresholds, rather than the default’s thresholds that scale with the values the spring moves between. Over a small range — an opacity, a normalized progress value — that cuts the imperceptible tail short; over a large range, such as an angle in degrees, the fixed position threshold is the stricter one, so the spring runs a little longer. The role-specific configurations omit the field and get the default. Configs you write should supply the three numeric fields and leave it out.

SpringConfigs

Named configurations. Prefer one of these over hand-tuned numbers so related motion across an application stays coherent.
Four general-purpose presets, ordered from calmest to most energetic:
PresetCharacter
SOFT
Gentle. The least overshoot of the four; good for large or ambient movement.
STANDARD
The general-purpose choice. Clear overshoot, quick settle.
BOUNCY
Pronounced overshoot at a moderate speed. Use it when the bounce itself is the message.
SNAPPY
The same energetic character at much higher stiffness, so it covers the distance and settles fastest.
Six role-specific configurations, used by components and available when you are animating the same kind of thing:
ConfigRole
PROGRESS_VALUE
Determinate progress values. Settles onto the target with a slight overshoot.
CONTAINER_SCALE_PRESS
Content scale as a container enters the pressed state. Stiff and underdamped, so the scale passes the target and springs back. Container applies this itself.
CONTAINER_SCALE_RELEASE
Content scale as a container leaves the pressed state. A little looser than the press spring.
PARTIAL_FOCUS_HANDOFF
The partial-focus handoff between neighboring interactables as focus moves. Nearly critically damped, so the handoff reads as a lean rather than a bounce.
CHIP_LOADING_SWAP
Chip swapping into and out of its loading presentation.
CONTROL_TILE_ICON_SWAP
ControlTile swapping its icon. Looser damping, so the incoming icon settles with a visible bounce.

Behavior

Frame pacing. Values are produced from animation frames but emitted at most frameRate times per second, so a display running faster than the spring’s update rate does not multiply work. If a frame is delivered late, the step is clamped to a single interval, so a stalled tab or a dropped frame cannot make the spring jump ahead of where it should be.
Settling. When velocity and remaining distance both fall below the config’s thresholds, the value is set exactly to the target, onChange emits that exact value, and onRest fires once. After that isAnimating() is false and no frames are scheduled until the next setTarget.
Document visibility. Springs pause while the document is hidden and resume when it becomes visible again. All controllers share a single visibility listener.
Unmounting. The hook cancels any scheduled frame when the component unmounts, so there is nothing to clean up and no callback fires afterwards.
Reduced motion. When the user prefers reduced motion, setTarget snaps straight to the target: onChange emits the target and onRest fires, with no frames scheduled. If the preference turns on while a spring is in flight, the spring snaps immediately. You do not need to branch on usePrefersReducedMotion() for a spring — see reduced motion.
Environments without animation frames. If requestAnimationFrame is unavailable, the controller snaps to the target instead of failing.
Validation. Invalid physics throws rather than silently producing NaN frames: mass and stiffness must be positive, damping must be zero or greater, frameRate must be finite and positive, and initialValue and every setTarget value must be finite.
  • Use one controller per animated value and choose a named SpringConfigs member so related motion stays coherent.
  • Write the value into a transform, opacity, or CSS custom property from onChange to avoid per-frame React renders.
  • Call setTarget from an event handler or effect whenever the logical target changes, including while another animation is running.
  • Use stop() when an interaction takes manual control, then setTarget to hand the value back to the spring.

Avoid

  • Write imperatively inside onChange; a React state setter there re-renders the subtree on every emitted frame.
  • Retarget the existing controller instead of recreating config values to restart an animation.
  • Let the toolkit components own properties they already animate, such as a container’s focus or press scale.
  • Prefer an interpolated transition for a fixed, uninterruptible A-to-B change; it is cheaper and more predictable.