How to Configure Spring Animations Using Apple's Approach: Damping, Response, and Interruptibility
Apple's motion design replaces the traditional physics triplet (mass, stiffness, damping) with designer-friendly damping ratio and response parameters, creating interruptible springs that preserve velocity during gesture reversals.
To configure spring animations using Apple's approach in web projects, reference the emilkowalski/skills repository, which documents how to translate Apple's iOS spring physics into web-friendly APIs like Motion and Framer Motion. This methodology treats animation as a physical conversation where UI elements retain momentum and can be re-targeted mid-flight without jarring interruptions.
Why Apple Replaced the Physics Triplet
According to skills/apple-design/SKILL.md, Apple deliberately moved away from the classic physics triplet of mass, stiffness, and damping. Instead, they optimize for two intuitive parameters:
- Damping ratio – Controls how much the spring oscillates (1.0 = critically damped, no bounce; 0.8 = slight overshoot for momentum-driven gestures)
- Response – Defines how quickly the spring settles (typically 0.3–0.4 seconds for standard UI)
This shift makes springs more predictable for designers while maintaining physical accuracy. The repository notes this is not equivalent to CSS animation duration—it is the characteristic time scale of the physical system.
Default Configurations for Different Interactions
The skills/apple-design/SKILL.md file provides specific defaults based on interaction type:
| Interaction | Damping | Response | Bounce |
|---|---|---|---|
| Move / Reposition | 1.0 | 0.4s | 0 |
| Momentum gestures (flick, drag-to-dismiss) | 0.8 | 0.4s | 0.2 |
For most UI elements, use a critically damped spring (damping = 1.0) to avoid overshoot. Reserve under-damped springs (damping ≈ 0.8) for momentum-driven interactions where a subtle bounce reinforces the physical metaphor.
Understanding Interruptibility and Velocity Hand-Off
A core principle documented in skills/apple-design/SKILL.md is interruptibility: springs must always start from the current presentation value (on-screen position) and carry existing velocity when re-targeted. This prevents the "brick wall" effect common in CSS keyframe animations.
When implementing gesture-driven springs, calculate the release velocity and pass it directly to the animation engine:
// Calculate velocity from last 5 pointer events
const velocity = (currentPos - lastPos) / (currentTime - lastTime);
// Pass to spring (Motion/Framer Motion accepts px/s)
animate(element, { y: targetY }, {
type: 'spring',
bounce: 0.2,
duration: 0.4,
velocity // Preserves gesture momentum
});
If your library expects relative velocity rather than absolute, normalize it using gestureVelocity / (target - current) as noted in the velocity hand-off section of the Apple design skill.
Configuring Springs in Web Frameworks
The Apple-Style Default Configuration
The canonical configuration recorded in skills/improve-animations/AUDIT.md uses Motion's bounce and duration API to map Apple's damping and response:
import { animate } from 'motion';
// Standard UI spring (critically damped)
animate(element, { y: 0 }, {
type: 'spring',
bounce: 0,
duration: 0.4
});
// Momentum-driven interaction (slight bounce)
animate(element, { y: target }, {
type: 'spring',
bounce: 0.2,
duration: 0.5
});
Full Physics Control
For fine-tuned control when the simplified API doesn't suffice, use the full physics form as shown in skills/animate/SKILL.md:
animate(element, { x: 150 }, {
type: 'spring',
mass: 1,
stiffness: 100,
damping: 10 // Under-damped = visible bounce
});
Velocity Hand-Off Implementation
Complete implementation combining gesture tracking with velocity preservation:
let moveHistory = []; // Store last 5 pointer events
element.addEventListener('pointermove', (e) => {
moveHistory.push({ x: e.clientY, t: performance.now() });
if (moveHistory.length > 5) moveHistory.shift();
});
element.addEventListener('pointerup', (e) => {
const now = performance.now();
const oldest = moveHistory[0];
const velocity = (e.clientY - oldest.x) / (now - oldest.t);
animate(element, { y: 0 }, {
type: 'spring',
bounce: 0.2,
duration: 0.4,
velocity // Maintains physical continuity
});
});
Accessibility and Reduced Motion Support
Per skills/apple-design/SKILL.md, every spring animation must respect prefers-reduced-motion. When this media query is active, replace springs with short opacity cross-fades:
import { useReducedMotion } from 'framer-motion';
const shouldReduceMotion = useReducedMotion();
const animationOptions = shouldReduceMotion
? { type: 'tween', duration: 0.2, ease: 'linear' }
: { type: 'spring', bounce: 0.2, duration: 0.4 };
animate(element, { y: target }, animationOptions);
Summary
- Use damping ratio and response instead of mass/stiffness/damping for Apple-style springs, as documented in
skills/apple-design/SKILL.md. - Configure critically damped springs (
bounce: 0,duration: 0.4) for standard UI moves and under-damped springs (bounce: 0.2) for momentum gestures. - Preserve interruptibility by starting animations from the current presentation value and carrying velocity through gesture reversals.
- Hand off release velocity calculated from pointer history to maintain physical continuity when gestures end.
- Support reduced motion by falling back to opacity fades when users prefer minimal animation.
Frequently Asked Questions
What is the difference between Apple's spring parameters and traditional physics springs?
Apple's approach uses damping ratio and response rather than mass, stiffness, and damping. Damping ratio (0.0–1.0) intuitively controls bounciness, while response defines the animation's speed without being a fixed duration. This makes springs easier to tune for designers while maintaining physical accuracy, as implemented in the emilkowalski/skills repository's web mapping guidelines.
How do I convert between Apple's damping ratio and Motion's bounce parameter?
In Motion or Framer Motion, bounce maps roughly to 1 - dampingRatio. A critically damped Apple spring (damping = 1.0) corresponds to bounce: 0, while a momentum-driven spring (damping ≈ 0.8) maps to bounce: 0.2. The canonical Apple-style configuration in skills/improve-animations/AUDIT.md recommends { type: "spring", duration: 0.5, bounce: 0.2 } for interactive elements.
When should I use a spring animation versus a standard tween?
Reach for springs for anything the user can touch, according to skills/animate/SKILL.md. This includes draggable elements, flick-to-dismiss gestures, scroll physics, and any UI that should feel alive and interruptible. Use standard tweens only for decorative transitions that don't respond to user input or when prefers-reduced-motion is active.
How do I prevent springs from jumping when a new gesture starts?
Ensure your animation library starts from the presentation value (current on-screen position) rather than the animation target. Pass the current velocity when re-targeting the spring. In Motion, this happens automatically if you call animate() on an element already in motion, but verify your velocity calculations use the formula gestureVelocity / (target - current) if the library expects relative velocity values.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →