React Native Spring Animations
A collection of spring physics animations for React Native — bounce, wobble, pulse, and elastic effects using Animated.spring with configurable tension and friction.
Expo Snack
Code
import React, { useRef, useCallback, useEffect } from "react";
import { Animated, Pressable, StyleSheet, Text, View, type ViewStyle } from "react-native";
/* ------------------------------------------------------------------ */
/* Spring Presets */
/* ------------------------------------------------------------------ */
interface SpringConfig {
tension: number;
friction: number;
useNativeDriver: boolean;
}
const SpringPresets: Record<string, SpringConfig> = {
bouncy: { tension: 180, friction: 6, useNativeDriver: true },
gentle: { tension: 40, friction: 10, useNativeDriver: true },
wobbly: { tension: 120, friction: 4, useNativeDriver: true },
stiff: { tension: 300, friction: 20, useNativeDriver: true },
};
/* ------------------------------------------------------------------ */
/* useSpring hook */
/* ------------------------------------------------------------------ */
function useSpring(initialValue = 0) {
const value = useRef(new Animated.Value(initialValue)).current;
const animate = useCallback(
(toValue: number, config?: Partial<SpringConfig>) => {
Animated.spring(value, {
toValue,
tension: 120,
friction: 8,
useNativeDriver: true,
...config,
}).start();
},
[value]
);
return { value, animate };
}
/* ------------------------------------------------------------------ */
/* SpringBox */
/* ------------------------------------------------------------------ */
interface SpringBoxProps {
preset: keyof typeof SpringPresets;
label: string;
color: string;
style?: ViewStyle;
}
function SpringBox({ preset, label, color, style }: SpringBoxProps) {
const scale = useRef(new Animated.Value(1)).current;
const translateY = useRef(new Animated.Value(0)).current;
const rotate = useRef(new Animated.Value(0)).current;
const active = useRef(false);
const config = SpringPresets[preset];
const handlePress = () => {
const to = active.current ? 0 : 1;
active.current = !active.current;
Animated.parallel([
Animated.spring(scale, { toValue: to === 1 ? 1.2 : 1, ...config }),
Animated.spring(translateY, { toValue: to === 1 ? -20 : 0, ...config }),
Animated.spring(rotate, { toValue: to, ...config }),
]).start();
};
const rotateInterpolation = rotate.interpolate({
inputRange: [0, 1],
outputRange: ["0deg", "15deg"],
});
return (
<Pressable onPress={handlePress}>
<Animated.View
style={[
styles.box,
{ backgroundColor: color },
{
transform: [{ scale }, { translateY }, { rotate: rotateInterpolation }],
},
style,
]}
>
<Text style={styles.boxLabel}>{label}</Text>
<Text style={styles.presetLabel}>{preset}</Text>
</Animated.View>
</Pressable>
);
}
/* ------------------------------------------------------------------ */
/* PulseBox — looping scale animation */
/* ------------------------------------------------------------------ */
function PulseBox({ color, label }: { color: string; label: string }) {
const scale = useRef(new Animated.Value(1)).current;
useEffect(() => {
const pulse = Animated.loop(
Animated.sequence([
Animated.spring(scale, {
toValue: 1.15,
tension: 60,
friction: 3,
useNativeDriver: true,
}),
Animated.spring(scale, {
toValue: 1,
tension: 60,
friction: 3,
useNativeDriver: true,
}),
])
);
pulse.start();
return () => pulse.stop();
}, [scale]);
return (
<Animated.View style={[styles.box, { backgroundColor: color, transform: [{ scale }] }]}>
<Text style={styles.boxLabel}>{label}</Text>
<Text style={styles.presetLabel}>pulse (loop)</Text>
</Animated.View>
);
}
/* ------------------------------------------------------------------ */
/* ElasticSlideIn — slides in from the right with elastic spring */
/* ------------------------------------------------------------------ */
function ElasticSlideIn({ color, label }: { color: string; label: string }) {
const translateX = useRef(new Animated.Value(300)).current;
useEffect(() => {
Animated.spring(translateX, {
toValue: 0,
tension: 50,
friction: 5,
useNativeDriver: true,
}).start();
}, [translateX]);
return (
<Animated.View style={[styles.box, { backgroundColor: color, transform: [{ translateX }] }]}>
<Text style={styles.boxLabel}>{label}</Text>
<Text style={styles.presetLabel}>elastic slide-in</Text>
</Animated.View>
);
}
/* ------------------------------------------------------------------ */
/* Demo App */
/* ------------------------------------------------------------------ */
export default function App() {
return (
<View style={styles.container}>
<Text style={styles.title}>Spring Animations</Text>
<Text style={styles.subtitle}>Tap each box to trigger its spring</Text>
<View style={styles.column}>
<SpringBox preset="bouncy" label="Bounce" color="#6366f1" />
<SpringBox preset="wobbly" label="Wobble" color="#f59e0b" />
<PulseBox label="Pulse" color="#10b981" />
<ElasticSlideIn label="Elastic" color="#ef4444" />
</View>
</View>
);
}
/* ------------------------------------------------------------------ */
/* Styles */
/* ------------------------------------------------------------------ */
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#0f172a",
alignItems: "center",
justifyContent: "center",
paddingVertical: 40,
},
title: {
color: "#f8fafc",
fontSize: 24,
fontWeight: "700",
marginBottom: 4,
},
subtitle: {
color: "#94a3b8",
fontSize: 14,
marginBottom: 32,
},
column: {
gap: 20,
alignItems: "center",
},
box: {
width: 200,
height: 80,
borderRadius: 16,
alignItems: "center",
justifyContent: "center",
},
boxLabel: {
color: "#fff",
fontSize: 18,
fontWeight: "700",
},
presetLabel: {
color: "rgba(255,255,255,0.7)",
fontSize: 12,
marginTop: 2,
},
});Spring Animations
A set of spring physics animations for React Native powered by Animated.spring. Each preset configures tension and friction to produce a distinct feel — from a snappy bounce to a gentle wobble.
How it works
The useSpring hook wraps Animated.Value and exposes an animate(toValue, config?) method that fires Animated.spring with sensible defaults. Four built-in presets cover most use cases:
- Bouncy — high tension, low friction. Snaps into place with a strong bounce.
- Gentle — low tension. Eases in slowly with minimal overshoot.
- Wobbly — low friction. Oscillates several times before settling.
- Stiff — high tension, high friction. Arrives fast with almost no bounce.
Features
- Configurable tension and friction via presets or custom values
SpringBoxcomponent handles press-to-animate with scale, translateY, and rotate transforms- Looping pulse animation for attention-grabbing effects
- Zero external dependencies — uses only the built-in
AnimatedAPI
When to use it
- Button press feedback and micro-interactions
- Onboarding animations and attention indicators
- Card entrance and exit transitions
- Any motion that benefits from natural, physics-based easing