Animations & Gestures (Reanimated + Gesture Handler)
Polished apps feel alive — smooth 60fps motion and swipe/drag interactions come from Reanimated and Gesture Handler.
What you will learn
- Animate a value smoothly with Reanimated
- Understand why animations run on the UI thread
- Handle a drag gesture with Gesture Handler
Why apps need motion
The difference between a rough app and a polished one is often motion: a card that slides in, a button that springs when tapped, a sheet you can drag up. Smooth motion means 60fps — the screen updates 60 times a second so nothing stutters. Two libraries deliver this in React Native, and modern courses dedicate whole projects to them: Reanimated (for animations) and Gesture Handler (for swipe, drag and pinch).
There is a built-in Animated API too, but it can stutter because it runs animation logic on the same JavaScript thread that handles your app’s work (a "thread" is just a lane the phone runs code in). If that lane is busy, the animation hiccups. Reanimated runs the animation on the separate UI thread instead — the lane that draws the screen — so it stays smooth even when JavaScript is busy. That is why it is the modern default.
Install both (one-time):
npx expo install react-native-reanimated react-native-gesture-handlerNote: Output: (Adds both packages. In a bare project Reanimated needs a small Babel config line, but Expo sets this up for you.)
A shared value — the heart of Reanimated
Reanimated’s key idea is a shared value: a special number that can be read and changed on the UI thread, so animating it never touches the slow JavaScript lane. You create one with useSharedValue, then connect it to a component’s style with useAnimatedStyle. Here a box fades and grows when you tap:
import Animated, {
useSharedValue, useAnimatedStyle, withTiming,
} from 'react-native-reanimated';
import { Button, View } from 'react-native';
export default function App() {
const scale = useSharedValue(1); // a UI-thread number
const boxStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }], // style follows the value
}));
return (
<View>
<Animated.View style={[{ width: 100, height: 100, backgroundColor: 'tomato' }, boxStyle]} />
<Button title="Grow" onPress={() => { scale.value = withTiming(2); }} />
</View>
);
}Note: Output:
A red box. Tap "Grow" and it smoothly doubles in size over a fraction of a second (not an instant jump).
withTiming(2) means "animate the value to 2 over time" instead of setting it instantly. Because scale is a shared value, this runs on the UI thread and stays at 60fps.
The moving parts, in plain terms:
useSharedValue(1)creates an animatable number starting at 1; you read and write it asscale.value.useAnimatedStyle(() => ...)builds a style that re-computes whenever the shared value changes — here the box’sscale.Animated.Viewis the special version ofViewthat can use an animated style.withTiming(2)is an animation helper: it eases the value to 2 smoothly instead of snapping.withSpringgives a bouncy feel.
Handling a drag gesture
Gesture Handler lets you respond to touch beyond a simple tap: drag (pan), swipe, pinch, long-press. You wrap your whole app once in a GestureHandlerRootView, define a gesture with the Gesture builder, and attach it with GestureDetector. Here a box follows your finger as you drag it:
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import Animated, { useSharedValue, useAnimatedStyle } from 'react-native-reanimated';
function DraggableBox() {
const x = useSharedValue(0);
const y = useSharedValue(0);
const drag = Gesture.Pan().onChange((e) => {
x.value += e.changeX; // move by how far the finger moved
y.value += e.changeY;
});
const style = useAnimatedStyle(() => ({
transform: [{ translateX: x.value }, { translateY: y.value }],
}));
return (
<GestureDetector gesture={drag}>
<Animated.View style={[{ width: 80, height: 80, backgroundColor: 'royalblue' }, style]} />
</GestureDetector>
);
}Note: Output:
A blue box you can drag anywhere on the screen with your finger; it follows smoothly and stays where you let go.
Gesture.Pan() tracks a finger drag; onChange fires as it moves, giving changeX/changeY (how far since the last update), which we add to the shared values driving the box’s position.
How a gesture-driven animation fits together, step by step
Here is the full path from finger to pixels for the draggable box, in order:
- You wrap the app once in
<GestureHandlerRootView style={{ flex: 1 }}>so gestures work anywhere inside it. - You create shared values
xandyfor the box’s position — UI-thread numbers, so updating them never stutters. - You build a gesture with
Gesture.Pan()and describe what to do on each move inonChange. - You attach it by wrapping the box in
<GestureDetector gesture={drag}>. - As the user drags,
onChangeruns on the UI thread and adds the finger’s movement toxandy. - Because
useAnimatedStylewatches those shared values, the box’stranslateX/translateYupdate instantly — so it tracks the finger at 60fps without involving the slow JavaScript lane.
Tip: Start with the ready-made helpers before hand-rolling animations: withTiming (steady) and withSpring (bouncy) cover most needs, and Gesture.Pan / Gesture.Tap / Gesture.Pinch cover most interactions. Reach for custom math only when those are not enough.
Watch out: Inside useAnimatedStyle and gesture callbacks you are on the UI thread, where normal React state setters do not belong. Drive motion with shared values (scale.value = ...), not useState — using setState for per-frame animation defeats the whole point and stutters.
Q. Why does Reanimated stay smooth when the built-in Animated API can stutter?
✍️ Practice
- Animate a box’s
opacityfrom 0 to 1 withwithTimingwhen a button is tapped. - Make a
Gesture.Tap()that bounces a box withwithSpringon each tap.
🏠 Homework
- Build a draggable card with Gesture Handler that, when dragged far enough sideways, animates off-screen with
withTiming(a simple swipe-to-dismiss).