A spring is how things move in real life. When you pull a rubber band and let go, it doesn't stop instantly. It snaps back, wobbles a bit, then settles. Flutter lets you make animations move like that instead of moving at a fixed speed.
mass is how heavy the thing is. Heavier = slower to start, slower to stop. Like pushing a bowling ball vs a tennis ball.
Low mass (0.3): quick, light, snappy
High mass (2.0): slow, heavy, lazy
stiffness is how strong the spring pulls back. Like a rubber band, thin one is weak, thick one is strong.
Low stiffness (50): slow, soft, takes time to settle
High stiffness (500): fast, sharp, snaps into place quickly
damping is how fast the bouncing stops. Like bouncing a ball on sand vs concrete.
Low damping (3): bounces many times before stopping
High damping (30): barely bounces, settles almost immediately
Zero damping: bounces forever (never use this)
The most direct way to drive a spring is through an AnimationController. Hand animateWith a SpringSimulation and the controller carries the value from start to end on the spring's curve:
Use it in a gesture's onEnd, or anywhere you'd call .forward() / .animateTo(). Pass the release velocity in and the spring carries the user's motion through, so, for example, drag-to-dismiss sheets and swipeable cards feel alive instead of mechanical.
One catch: use AnimationController.unbounded(vsync: this). A plain AnimationController clamps values to [0, 1] and silently eats the spring's overshoot, so the bounce disappears.
The same spring drops into anything scrollable. Every scrollable widget accepts a physics param, and inside that physics there's a spring getter you can override:
For snapping widgets, you feel the spring every swipe. For free-scrolling lists, the spring drives edge bounce-back, but only under BouncingScrollPhysics (iOS default). Android's ClampingScrollPhysics glows instead.
Route transitions are the third surface. Override TransitionRoute.createSimulation() to return a SpringSimulation instead of a duration-based curve, and the route enters and exits on the spring.
There are no correct values. Every app is different. Tweak until it feels right. If hand-tuning sounds like work, the motor package ships presets matching each platform's design system:
// iOS feelSingleMotionBuilder( // autocomplete to see other variants too motion: CupertinoMotion.bouncy(), value: target, builder: (context, v, _) { return Transform.translate( offset: Offset(v, 0), child: child, ); },);// Material 3 feelSingleMotionBuilder( // autocomplete to see other variants too motion: MaterialSpringMotion.expressiveSpatialDefault(), value: target, builder: (context, v, _) { return Transform.translate( offset: Offset(v, 0), child: child, ); },);
MaterialSpringMotion splits spatial (position, size) from effects (opacity, color), each available in standard or expressive at fast, default, or slow. Match the platform you're emulating, or mix on purpose.