Module 9: Animations and Gestures
Layout Animations
Animate components as they enter, exit, and transition within your layouts
🎯 Learning Objectives
- Understand layout animations and when to use them
- Implement entering animations for mounting components
- Create exiting animations for unmounting components
- Use layout transitions for smooth position changes
- Build custom keyframe animations
- Apply layout animations to lists and dynamic content
Introduction to Layout Animations
Layout animations bring your UI to life by animating components as they appear, disappear, or change position. Unlike imperative animations where you manually control values, layout animations are declarative—you specify what should happen, and Reanimated handles the how.
Types of Layout Animations
flowchart LR
subgraph Entering["Entering"]
A[Component mounts]
B[Animate in]
end
subgraph Layout["Layout"]
C[Position changes]
D[Animate to new position]
end
subgraph Exiting["Exiting"]
E[Component unmounts]
F[Animate out first]
end
Entering --> Layout --> Exiting
style Entering fill:#e8f5e9
style Layout fill:#e3f2fd
style Exiting fill:#ffebee
When to Use Layout Animations
| Scenario | Animation Type | Example |
|---|---|---|
| New item appears | Entering | Toast notification slides in |
| Item is removed | Exiting | Deleted list item fades out |
| List reorders | Layout | Items smoothly shift positions |
| Size changes | Layout | Accordion expands/collapses |
| Screen transition | Entering + Exiting | Page content fades between screens |
Basic Usage Pattern
import Animated, {
FadeIn,
FadeOut,
Layout,
} from 'react-native-reanimated';
function AnimatedComponent({ visible }: { visible: boolean }) {
if (!visible) return null;
return (
<Animated.View
entering={FadeIn} // Animation when mounting
exiting={FadeOut} // Animation when unmounting
layout={Layout.springify()} // Animation when position changes
style={styles.box}
>
<Text>Hello!</Text>
</Animated.View>
);
}
💡 Key Concept
Layout animations are applied as props to Animated.View components. They automatically trigger based on component lifecycle:
- entering: Runs when component mounts
- exiting: Runs when component unmounts (delays removal)
- layout: Runs when layout position or size changes
Entering Animations
Entering animations run when a component mounts. Reanimated provides many built-in presets that you can use directly or customize.
Built-in Entering Animations
import Animated, {
// Fade animations
FadeIn,
FadeInUp,
FadeInDown,
FadeInLeft,
FadeInRight,
// Slide animations
SlideInUp,
SlideInDown,
SlideInLeft,
SlideInRight,
// Zoom animations
ZoomIn,
ZoomInUp,
ZoomInDown,
ZoomInLeft,
ZoomInRight,
ZoomInRotate,
ZoomInEasyUp,
ZoomInEasyDown,
// Bounce animations
BounceIn,
BounceInUp,
BounceInDown,
BounceInLeft,
BounceInRight,
// Flip animations
FlipInXUp,
FlipInXDown,
FlipInYLeft,
FlipInYRight,
FlipInEasyX,
FlipInEasyY,
// Stretch animations
StretchInX,
StretchInY,
// LightSpeed animations
LightSpeedInLeft,
LightSpeedInRight,
// Pinwheel
PinwheelIn,
// Roll
RollInLeft,
RollInRight,
// Rotate
RotateInUpLeft,
RotateInUpRight,
RotateInDownLeft,
RotateInDownRight,
} from 'react-native-reanimated';
// Simple usage
function EnteringExample() {
return (
<Animated.View entering={FadeInUp}>
<Text>I fade in from above!</Text>
</Animated.View>
);
}
Customizing Entering Animations
import Animated, {
FadeIn,
SlideInRight,
ZoomIn,
BounceIn,
} from 'react-native-reanimated';
function CustomEnteringExample() {
return (
<View>
{/* Custom duration */}
<Animated.View entering={FadeIn.duration(800)}>
<Text>Slow fade in (800ms)</Text>
</Animated.View>
{/* Custom delay */}
<Animated.View entering={SlideInRight.delay(300)}>
<Text>Delayed slide in</Text>
</Animated.View>
{/* Spring physics */}
<Animated.View entering={ZoomIn.springify()}>
<Text>Bouncy zoom in</Text>
</Animated.View>
{/* Custom spring config */}
<Animated.View
entering={BounceIn.springify()
.damping(12)
.stiffness(100)
.mass(0.5)}
>
<Text>Custom spring bounce</Text>
</Animated.View>
{/* Chained customizations */}
<Animated.View
entering={FadeIn
.delay(200)
.duration(500)
.withInitialValues({ opacity: 0.5 })}
>
<Text>Start from 50% opacity</Text>
</Animated.View>
</View>
);
}
Staggered Entering
import Animated, { FadeInUp } from 'react-native-reanimated';
function StaggeredList() {
const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'];
return (
<View>
{items.map((item, index) => (
<Animated.View
key={item}
entering={FadeInUp.delay(index * 100).springify()}
style={styles.listItem}
>
<Text>{item}</Text>
</Animated.View>
))}
</View>
);
}
Callback on Animation Complete
import Animated, { FadeIn, runOnJS } from 'react-native-reanimated';
function CallbackExample() {
const [animationComplete, setAnimationComplete] = useState(false);
const handleAnimationComplete = () => {
setAnimationComplete(true);
console.log('Entering animation finished!');
};
return (
<Animated.View
entering={FadeIn.duration(500).withCallback((finished) => {
'worklet';
if (finished) {
runOnJS(handleAnimationComplete)();
}
})}
>
<Text>Watch for callback</Text>
</Animated.View>
);
}
Custom Entering Animation
import Animated, { withTiming, withSpring } from 'react-native-reanimated';
// Define a custom entering animation
const CustomEntering = (targetValues) => {
'worklet';
const animations = {
opacity: withTiming(1, { duration: 500 }),
transform: [
{ translateY: withSpring(0, { damping: 15 }) },
{ scale: withSpring(1, { damping: 12 }) },
{ rotate: withTiming('0deg', { duration: 400 }) },
],
};
const initialValues = {
opacity: 0,
transform: [
{ translateY: 100 },
{ scale: 0.5 },
{ rotate: '-45deg' },
],
};
return {
initialValues,
animations,
};
};
// Use the custom animation
function CustomEnteringExample() {
return (
<Animated.View entering={CustomEntering} style={styles.box}>
<Text>Custom animation!</Text>
</Animated.View>
);
}
Entering Animation Reference
| Category | Animations | Best For |
|---|---|---|
| Fade | FadeIn, FadeInUp/Down/Left/Right |
Subtle, elegant appearance |
| Slide | SlideInUp/Down/Left/Right |
Drawers, sheets, menus |
| Zoom | ZoomIn, ZoomInRotate |
Modals, popups, emphasis |
| Bounce | BounceIn, BounceInUp/Down |
Playful UI, notifications |
| Flip | FlipInX/Y, FlipInEasyX/Y |
Card reveals, transitions |
Exiting Animations
Exiting animations run when a component unmounts. They delay the actual removal until the animation completes, creating smooth transitions.
Built-in Exiting Animations
import Animated, {
// Fade animations
FadeOut,
FadeOutUp,
FadeOutDown,
FadeOutLeft,
FadeOutRight,
// Slide animations
SlideOutUp,
SlideOutDown,
SlideOutLeft,
SlideOutRight,
// Zoom animations
ZoomOut,
ZoomOutUp,
ZoomOutDown,
ZoomOutLeft,
ZoomOutRight,
ZoomOutRotate,
ZoomOutEasyUp,
ZoomOutEasyDown,
// Bounce animations
BounceOut,
BounceOutUp,
BounceOutDown,
BounceOutLeft,
BounceOutRight,
// Flip animations
FlipOutXUp,
FlipOutXDown,
FlipOutYLeft,
FlipOutYRight,
FlipOutEasyX,
FlipOutEasyY,
// Other
StretchOutX,
StretchOutY,
LightSpeedOutLeft,
LightSpeedOutRight,
PinwheelOut,
RollOutLeft,
RollOutRight,
RotateOutUpLeft,
RotateOutUpRight,
RotateOutDownLeft,
RotateOutDownRight,
} from 'react-native-reanimated';
function ExitingExample() {
const [visible, setVisible] = useState(true);
return (
<View>
<Pressable onPress={() => setVisible(!visible)}>
<Text>Toggle</Text>
</Pressable>
{visible && (
<Animated.View
entering={FadeIn}
exiting={FadeOutDown.duration(300)}
style={styles.box}
>
<Text>I fade out downward!</Text>
</Animated.View>
)}
</View>
);
}
Matching Entering and Exiting
import Animated, {
SlideInRight,
SlideOutRight,
ZoomIn,
ZoomOut,
FadeInUp,
FadeOutDown,
} from 'react-native-reanimated';
function MatchedAnimations() {
const [items, setItems] = useState(['a', 'b', 'c']);
const addItem = () => {
setItems([...items, Date.now().toString()]);
};
const removeItem = (id: string) => {
setItems(items.filter(item => item !== id));
};
return (
<View>
{items.map((item) => (
<Animated.View
key={item}
entering={SlideInRight.springify()}
exiting={SlideOutRight.duration(200)}
style={styles.item}
>
<Text>{item}</Text>
<Pressable onPress={() => removeItem(item)}>
<Text>×</Text>
</Pressable>
</Animated.View>
))}
<Pressable onPress={addItem}>
<Text>Add Item</Text>
</Pressable>
</View>
);
}
Custom Exiting Animation
import Animated, { withTiming, withSpring } from 'react-native-reanimated';
const CustomExiting = (values) => {
'worklet';
const animations = {
opacity: withTiming(0, { duration: 300 }),
transform: [
{ translateX: withTiming(values.currentWidth, { duration: 300 }) },
{ scale: withTiming(0.8, { duration: 300 }) },
{ rotate: withTiming('15deg', { duration: 300 }) },
],
};
const initialValues = {
opacity: 1,
transform: [
{ translateX: 0 },
{ scale: 1 },
{ rotate: '0deg' },
],
};
return {
initialValues,
animations,
};
};
function CustomExitExample() {
const [visible, setVisible] = useState(true);
return (
<View>
{visible && (
<Animated.View
entering={FadeIn}
exiting={CustomExiting}
style={styles.box}
>
<Text>Custom exit animation!</Text>
</Animated.View>
)}
</View>
);
}
⚠️ Important Notes
- Exiting animations only work when the component is conditionally rendered
- The component must have a unique
keyprop for proper tracking - Parent components must be
Animated.Viewor wrapped properly - Exiting delays actual unmount—don't rely on immediate cleanup
Layout Transitions
Layout transitions animate components when their position or size changes within the layout. This creates fluid, natural-feeling interfaces where elements smoothly shift to accommodate changes.
Basic Layout Transition
import Animated, { Layout } from 'react-native-reanimated';
function LayoutTransitionExample() {
const [expanded, setExpanded] = useState(false);
return (
<View>
<Pressable onPress={() => setExpanded(!expanded)}>
<Animated.View
layout={Layout.springify()}
style={[
styles.box,
expanded && styles.expandedBox,
]}
>
<Text>{expanded ? 'Expanded' : 'Tap to expand'}</Text>
</Animated.View>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
box: {
width: 100,
height: 100,
backgroundColor: '#007AFF',
justifyContent: 'center',
alignItems: 'center',
},
expandedBox: {
width: 200,
height: 200,
},
});
Layout Animation Types
import Animated, {
Layout,
LinearTransition,
SequencedTransition,
FadingTransition,
JumpingTransition,
CurvedTransition,
EntryExitTransition,
Easing,
} from 'react-native-reanimated';
function LayoutTypesExample() {
return (
<View>
{/* Default spring-based layout */}
<Animated.View layout={Layout}>
<Text>Default Layout</Text>
</Animated.View>
{/* Spring with customization */}
<Animated.View
layout={Layout.springify().damping(15).stiffness(100)}
>
<Text>Springy Layout</Text>
</Animated.View>
{/* Linear timing-based */}
<Animated.View layout={LinearTransition.duration(300)}>
<Text>Linear Transition</Text>
</Animated.View>
{/* With easing */}
<Animated.View
layout={LinearTransition
.duration(400)
.easing(Easing.bezier(0.25, 0.1, 0.25, 1))}
>
<Text>Eased Transition</Text>
</Animated.View>
{/* Sequenced (width then height) */}
<Animated.View layout={SequencedTransition}>
<Text>Sequenced Transition</Text>
</Animated.View>
{/* Fading transition */}
<Animated.View layout={FadingTransition}>
<Text>Fading Transition</Text>
</Animated.View>
{/* Jumping transition */}
<Animated.View layout={JumpingTransition}>
<Text>Jumping Transition</Text>
</Animated.View>
{/* Curved transition */}
<Animated.View layout={CurvedTransition}>
<Text>Curved Transition</Text>
</Animated.View>
</View>
);
}
Reordering Items
import React, { useState } from 'react';
import { StyleSheet, View, Text, Pressable } from 'react-native';
import Animated, { Layout, FadeIn, FadeOut } from 'react-native-reanimated';
interface Item {
id: string;
title: string;
color: string;
}
function ReorderingList() {
const [items, setItems] = useState<Item[]>([
{ id: '1', title: 'Red', color: '#FF3B30' },
{ id: '2', title: 'Orange', color: '#FF9500' },
{ id: '3', title: 'Yellow', color: '#FFCC00' },
{ id: '4', title: 'Green', color: '#34C759' },
{ id: '5', title: 'Blue', color: '#007AFF' },
]);
const shuffle = () => {
setItems([...items].sort(() => Math.random() - 0.5));
};
const reverse = () => {
setItems([...items].reverse());
};
return (
<View style={styles.container}>
<View style={styles.buttons}>
<Pressable style={styles.button} onPress={shuffle}>
<Text style={styles.buttonText}>Shuffle</Text>
</Pressable>
<Pressable style={styles.button} onPress={reverse}>
<Text style={styles.buttonText}>Reverse</Text>
</Pressable>
</View>
<View style={styles.list}>
{items.map((item) => (
<Animated.View
key={item.id}
layout={Layout.springify().damping(15)}
style={[styles.item, { backgroundColor: item.color }]}
>
<Text style={styles.itemText}>{item.title}</Text>
</Animated.View>
))}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
buttons: {
flexDirection: 'row',
marginBottom: 20,
gap: 10,
},
button: {
backgroundColor: '#333',
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 8,
},
buttonText: {
color: 'white',
fontWeight: '600',
},
list: {
gap: 10,
},
item: {
padding: 20,
borderRadius: 12,
alignItems: 'center',
},
itemText: {
color: 'white',
fontSize: 18,
fontWeight: '600',
},
});
Accordion with Layout Animation
import React, { useState } from 'react';
import { StyleSheet, View, Text, Pressable } from 'react-native';
import Animated, {
Layout,
FadeIn,
FadeOut,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
interface AccordionProps {
title: string;
children: React.ReactNode;
}
function Accordion({ title, children }: AccordionProps) {
const [expanded, setExpanded] = useState(false);
const rotation = useSharedValue(0);
const toggle = () => {
setExpanded(!expanded);
rotation.value = withTiming(expanded ? 0 : 90, { duration: 200 });
};
const chevronStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value}deg` }],
}));
return (
<Animated.View layout={Layout.springify()} style={styles.accordion}>
<Pressable onPress={toggle} style={styles.header}>
<Text style={styles.title}>{title}</Text>
<Animated.Text style={[styles.chevron, chevronStyle]}>
▶
</Animated.Text>
</Pressable>
{expanded && (
<Animated.View
entering={FadeIn.duration(200)}
exiting={FadeOut.duration(200)}
style={styles.content}
>
{children}
</Animated.View>
)}
</Animated.View>
);
}
function AccordionExample() {
return (
<View style={styles.container}>
<Accordion title="Section 1">
<Text>Content for section 1. This can be any length.</Text>
</Accordion>
<Accordion title="Section 2">
<Text>Content for section 2.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Sed do eiusmod tempor incididunt ut labore.
</Text>
</Accordion>
<Accordion title="Section 3">
<Text>Short content.</Text>
</Accordion>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
gap: 8,
},
accordion: {
backgroundColor: 'white',
borderRadius: 12,
overflow: 'hidden',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
},
title: {
fontSize: 16,
fontWeight: '600',
},
chevron: {
fontSize: 12,
color: '#666',
},
content: {
padding: 16,
paddingTop: 0,
},
});
Grid Layout Animation
import React, { useState } from 'react';
import { StyleSheet, View, Pressable, Text, Dimensions } from 'react-native';
import Animated, { Layout, FadeIn, FadeOut } from 'react-native-reanimated';
const { width } = Dimensions.get('window');
const COLUMNS = 3;
const GAP = 10;
const ITEM_SIZE = (width - 40 - (COLUMNS - 1) * GAP) / COLUMNS;
function AnimatedGrid() {
const [items, setItems] = useState(
Array.from({ length: 9 }, (_, i) => ({
id: `item-${i}`,
visible: true,
}))
);
const toggleItem = (id: string) => {
setItems(items.map(item =>
item.id === id ? { ...item, visible: !item.visible } : item
));
};
const visibleItems = items.filter(item => item.visible);
return (
<View style={styles.container}>
<View style={styles.grid}>
{visibleItems.map((item) => (
<Animated.View
key={item.id}
layout={Layout.springify().damping(15)}
entering={FadeIn.springify()}
exiting={FadeOut.duration(200)}
>
<Pressable
style={styles.gridItem}
onPress={() => toggleItem(item.id)}
>
<Text style={styles.itemText}>×</Text>
</Pressable>
</Animated.View>
))}
</View>
<Pressable
style={styles.resetButton}
onPress={() => setItems(items.map(item => ({ ...item, visible: true })))}
>
<Text style={styles.resetText}>Reset All</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: GAP,
},
gridItem: {
width: ITEM_SIZE,
height: ITEM_SIZE,
backgroundColor: '#007AFF',
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
},
itemText: {
color: 'white',
fontSize: 24,
},
resetButton: {
marginTop: 20,
backgroundColor: '#34C759',
padding: 16,
borderRadius: 12,
alignItems: 'center',
},
resetText: {
color: 'white',
fontWeight: '600',
},
});
Keyframe Animations
Keyframe animations let you define multi-step animations with precise control over timing and values at each step.
Basic Keyframe Animation
import Animated, { Keyframe } from 'react-native-reanimated';
// Define a keyframe animation
const bounceKeyframe = new Keyframe({
0: {
transform: [{ scale: 0 }],
opacity: 0,
},
25: {
transform: [{ scale: 1.2 }],
opacity: 1,
},
50: {
transform: [{ scale: 0.9 }],
},
75: {
transform: [{ scale: 1.1 }],
},
100: {
transform: [{ scale: 1 }],
},
}).duration(800);
function KeyframeExample() {
const [visible, setVisible] = useState(true);
return (
<View>
<Pressable onPress={() => setVisible(!visible)}>
<Text>Toggle</Text>
</Pressable>
{visible && (
<Animated.View
entering={bounceKeyframe}
style={styles.box}
>
<Text>Bounce In!</Text>
</Animated.View>
)}
</View>
);
}
Complex Keyframe Animations
import Animated, { Keyframe, Easing } from 'react-native-reanimated';
// Shake animation
const shakeKeyframe = new Keyframe({
0: { transform: [{ translateX: 0 }] },
10: { transform: [{ translateX: -10 }] },
20: { transform: [{ translateX: 10 }] },
30: { transform: [{ translateX: -10 }] },
40: { transform: [{ translateX: 10 }] },
50: { transform: [{ translateX: -5 }] },
60: { transform: [{ translateX: 5 }] },
70: { transform: [{ translateX: -2 }] },
80: { transform: [{ translateX: 2 }] },
100: { transform: [{ translateX: 0 }] },
}).duration(500);
// Attention pulse
const pulseKeyframe = new Keyframe({
0: {
transform: [{ scale: 1 }],
opacity: 1,
},
50: {
transform: [{ scale: 1.05 }],
opacity: 0.8,
},
100: {
transform: [{ scale: 1 }],
opacity: 1,
},
}).duration(1000);
// Swing animation
const swingKeyframe = new Keyframe({
0: { transform: [{ rotate: '0deg' }] },
20: { transform: [{ rotate: '15deg' }] },
40: { transform: [{ rotate: '-10deg' }] },
60: { transform: [{ rotate: '5deg' }] },
80: { transform: [{ rotate: '-5deg' }] },
100: { transform: [{ rotate: '0deg' }] },
}).duration(800);
// Flip animation
const flipKeyframe = new Keyframe({
0: {
transform: [{ perspective: 400 }, { rotateY: '0deg' }],
opacity: 1,
},
40: {
transform: [{ perspective: 400 }, { rotateY: '-180deg' }],
opacity: 0,
},
60: {
transform: [{ perspective: 400 }, { rotateY: '-180deg' }],
opacity: 0,
},
100: {
transform: [{ perspective: 400 }, { rotateY: '-360deg' }],
opacity: 1,
},
}).duration(1000);
// Heartbeat animation
const heartbeatKeyframe = new Keyframe({
0: { transform: [{ scale: 1 }] },
14: { transform: [{ scale: 1.3 }] },
28: { transform: [{ scale: 1 }] },
42: { transform: [{ scale: 1.3 }] },
70: { transform: [{ scale: 1 }] },
100: { transform: [{ scale: 1 }] },
}).duration(1300);
Keyframe with Easing
import Animated, { Keyframe, Easing } from 'react-native-reanimated';
// Keyframe with custom easing per segment
const elasticEnterKeyframe = new Keyframe({
0: {
transform: [{ translateY: -100 }, { scale: 0 }],
opacity: 0,
easing: Easing.out(Easing.exp),
},
60: {
transform: [{ translateY: 20 }, { scale: 1.1 }],
opacity: 1,
easing: Easing.out(Easing.bounce),
},
100: {
transform: [{ translateY: 0 }, { scale: 1 }],
opacity: 1,
},
}).duration(1000);
// Delayed keyframe
const delayedBounceKeyframe = new Keyframe({
0: {
transform: [{ translateY: 0 }],
},
50: {
transform: [{ translateY: 0 }],
},
65: {
transform: [{ translateY: -30 }],
},
80: {
transform: [{ translateY: 0 }],
},
90: {
transform: [{ translateY: -15 }],
},
100: {
transform: [{ translateY: 0 }],
},
}).duration(1500);
Reusable Keyframe Components
import React from 'react';
import Animated, { Keyframe } from 'react-native-reanimated';
// Define animation keyframes
const animations = {
fadeInUp: new Keyframe({
0: {
opacity: 0,
transform: [{ translateY: 30 }],
},
100: {
opacity: 1,
transform: [{ translateY: 0 }],
},
}).duration(400),
fadeOutDown: new Keyframe({
0: {
opacity: 1,
transform: [{ translateY: 0 }],
},
100: {
opacity: 0,
transform: [{ translateY: 30 }],
},
}).duration(300),
popIn: new Keyframe({
0: {
transform: [{ scale: 0 }],
opacity: 0,
},
70: {
transform: [{ scale: 1.1 }],
opacity: 1,
},
100: {
transform: [{ scale: 1 }],
opacity: 1,
},
}).duration(350),
};
// Reusable animated wrapper
interface AnimatedItemProps {
animation?: keyof typeof animations;
delay?: number;
children: React.ReactNode;
}
function AnimatedItem({
animation = 'fadeInUp',
delay = 0,
children
}: AnimatedItemProps) {
const keyframe = animations[animation].delay(delay);
return (
<Animated.View entering={keyframe}>
{children}
</Animated.View>
);
}
// Usage
function AnimatedList() {
return (
<View>
<AnimatedItem delay={0}>
<Text>First</Text>
</AnimatedItem>
<AnimatedItem delay={100}>
<Text>Second</Text>
</AnimatedItem>
<AnimatedItem animation="popIn" delay={200}>
<Text>Third (pop)</Text>
</AnimatedItem>
</View>
);
}
💡 Keyframe Tips
- Percentages (0-100) define timing within the total duration
- You can skip percentages—intermediate values are interpolated
- Add
easingto individual keyframes for segment-specific easing - Use
.delay()to add a delay before the animation starts - Chain multiple keyframes with
.withCallback()for notifications
Animating Lists
Lists are one of the most common places to apply layout animations. Whether using FlatList, ScrollView, or simple maps, animations make list interactions feel polished and responsive.
Animated FlatList Items
import React, { useState, useCallback } from 'react';
import { StyleSheet, View, Text, FlatList, Pressable } from 'react-native';
import Animated, {
FadeInRight,
FadeOutLeft,
Layout,
} from 'react-native-reanimated';
interface ListItem {
id: string;
title: string;
}
function AnimatedFlatList() {
const [items, setItems] = useState<ListItem[]>([
{ id: '1', title: 'First Item' },
{ id: '2', title: 'Second Item' },
{ id: '3', title: 'Third Item' },
]);
const addItem = () => {
const newItem = {
id: Date.now().toString(),
title: `Item ${items.length + 1}`,
};
setItems([newItem, ...items]);
};
const removeItem = (id: string) => {
setItems(items.filter(item => item.id !== id));
};
const renderItem = useCallback(({ item, index }: { item: ListItem; index: number }) => (
<Animated.View
entering={FadeInRight.delay(index * 50).springify()}
exiting={FadeOutLeft.duration(200)}
layout={Layout.springify()}
style={styles.item}
>
<Text style={styles.itemText}>{item.title}</Text>
<Pressable
style={styles.deleteButton}
onPress={() => removeItem(item.id)}
>
<Text style={styles.deleteText}>Delete</Text>
</Pressable>
</Animated.View>
), []);
return (
<View style={styles.container}>
<Pressable style={styles.addButton} onPress={addItem}>
<Text style={styles.addText}>Add Item</Text>
</Pressable>
<FlatList
data={items}
renderItem={renderItem}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
addButton: {
backgroundColor: '#34C759',
padding: 16,
borderRadius: 12,
alignItems: 'center',
marginBottom: 16,
},
addText: {
color: 'white',
fontWeight: '600',
fontSize: 16,
},
list: {
gap: 8,
},
item: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: 'white',
padding: 16,
borderRadius: 12,
},
itemText: {
fontSize: 16,
},
deleteButton: {
backgroundColor: '#FF3B30',
paddingVertical: 8,
paddingHorizontal: 12,
borderRadius: 8,
},
deleteText: {
color: 'white',
fontWeight: '600',
},
});
Staggered List with Different Animations
import React, { useState, useEffect } from 'react';
import { StyleSheet, View, Text, ScrollView } from 'react-native';
import Animated, {
FadeInDown,
FadeInLeft,
FadeInRight,
ZoomIn,
BounceIn,
SlideInRight,
} from 'react-native-reanimated';
// Different animations for variety
const animations = [
FadeInDown,
FadeInLeft,
FadeInRight,
ZoomIn,
BounceIn,
SlideInRight,
];
interface CardData {
id: string;
title: string;
description: string;
}
function StaggeredCards() {
const [cards, setCards] = useState<CardData[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Simulate data fetch
setTimeout(() => {
setCards([
{ id: '1', title: 'Card 1', description: 'First card description' },
{ id: '2', title: 'Card 2', description: 'Second card description' },
{ id: '3', title: 'Card 3', description: 'Third card description' },
{ id: '4', title: 'Card 4', description: 'Fourth card description' },
{ id: '5', title: 'Card 5', description: 'Fifth card description' },
{ id: '6', title: 'Card 6', description: 'Sixth card description' },
]);
setLoading(false);
}, 500);
}, []);
if (loading) {
return (
<View style={styles.loading}>
<Text>Loading...</Text>
</View>
);
}
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{cards.map((card, index) => {
const Animation = animations[index % animations.length];
return (
<Animated.View
key={card.id}
entering={Animation.delay(index * 100).springify()}
style={styles.card}
>
<Text style={styles.cardTitle}>{card.title}</Text>
<Text style={styles.cardDescription}>{card.description}</Text>
</Animated.View>
);
})}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
padding: 16,
gap: 12,
},
loading: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
card: {
backgroundColor: 'white',
padding: 20,
borderRadius: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
cardTitle: {
fontSize: 18,
fontWeight: '600',
marginBottom: 8,
},
cardDescription: {
fontSize: 14,
color: '#666',
},
});
Swipeable List Item
import React, { useState } from 'react';
import { StyleSheet, View, Text, FlatList, Dimensions } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
runOnJS,
FadeIn,
FadeOut,
Layout,
} from 'react-native-reanimated';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const SWIPE_THRESHOLD = -80;
interface Task {
id: string;
title: string;
completed: boolean;
}
function SwipeableTask({
task,
onDelete,
onToggle,
}: {
task: Task;
onDelete: (id: string) => void;
onToggle: (id: string) => void;
}) {
const translateX = useSharedValue(0);
const panGesture = Gesture.Pan()
.activeOffsetX([-10, 10])
.onUpdate((event) => {
translateX.value = Math.min(0, Math.max(-120, event.translationX));
})
.onEnd((event) => {
if (translateX.value < SWIPE_THRESHOLD || event.velocityX < -500) {
translateX.value = withTiming(-120);
} else {
translateX.value = withSpring(0);
}
});
const handleDelete = () => {
translateX.value = withTiming(-SCREEN_WIDTH, { duration: 200 }, () => {
runOnJS(onDelete)(task.id);
});
};
const taskStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
return (
<Animated.View
entering={FadeIn.springify()}
exiting={FadeOut.duration(200)}
layout={Layout.springify()}
style={styles.taskContainer}
>
{/* Delete button behind */}
<View style={styles.deleteAction}>
<GestureDetector gesture={Gesture.Tap().onEnd(handleDelete)}>
<View style={styles.deleteButton}>
<Text style={styles.deleteText}>🗑️</Text>
</View>
</GestureDetector>
</View>
{/* Swipeable task */}
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.task, taskStyle]}>
<GestureDetector
gesture={Gesture.Tap().onEnd(() => onToggle(task.id))}
>
<View style={styles.checkbox}>
{task.completed && <Text>✓</Text>}
</View>
</GestureDetector>
<Text
style={[
styles.taskTitle,
task.completed && styles.completedTask
]}
>
{task.title}
</Text>
</Animated.View>
</GestureDetector>
</Animated.View>
);
}
function TaskList() {
const [tasks, setTasks] = useState<Task[]>([
{ id: '1', title: 'Buy groceries', completed: false },
{ id: '2', title: 'Call mom', completed: true },
{ id: '3', title: 'Finish project', completed: false },
{ id: '4', title: 'Go to gym', completed: false },
]);
const deleteTask = (id: string) => {
setTasks(tasks.filter(t => t.id !== id));
};
const toggleTask = (id: string) => {
setTasks(tasks.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
));
};
return (
<FlatList
data={tasks}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<SwipeableTask
task={item}
onDelete={deleteTask}
onToggle={toggleTask}
/>
)}
contentContainerStyle={styles.list}
/>
);
}
const styles = StyleSheet.create({
list: {
padding: 16,
gap: 8,
},
taskContainer: {
position: 'relative',
overflow: 'hidden',
borderRadius: 12,
},
deleteAction: {
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
width: 120,
backgroundColor: '#FF3B30',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 12,
},
deleteButton: {
padding: 20,
},
deleteText: {
fontSize: 24,
},
task: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'white',
padding: 16,
borderRadius: 12,
},
checkbox: {
width: 24,
height: 24,
borderWidth: 2,
borderColor: '#007AFF',
borderRadius: 6,
marginRight: 12,
justifyContent: 'center',
alignItems: 'center',
},
taskTitle: {
fontSize: 16,
flex: 1,
},
completedTask: {
textDecorationLine: 'line-through',
color: '#999',
},
});
Animated Section List
import React, { useState } from 'react';
import { StyleSheet, View, Text, SectionList, Pressable } from 'react-native';
import Animated, {
FadeInRight,
FadeOutLeft,
Layout,
SlideInDown,
} from 'react-native-reanimated';
interface Section {
title: string;
data: { id: string; name: string }[];
}
function AnimatedSectionList() {
const [sections, setSections] = useState<Section[]>([
{
title: 'Favorites',
data: [
{ id: '1', name: 'Item A' },
{ id: '2', name: 'Item B' },
],
},
{
title: 'Recent',
data: [
{ id: '3', name: 'Item C' },
{ id: '4', name: 'Item D' },
{ id: '5', name: 'Item E' },
],
},
{
title: 'All',
data: [
{ id: '6', name: 'Item F' },
{ id: '7', name: 'Item G' },
],
},
]);
const removeItem = (sectionTitle: string, itemId: string) => {
setSections(sections.map(section =>
section.title === sectionTitle
? { ...section, data: section.data.filter(item => item.id !== itemId) }
: section
).filter(section => section.data.length > 0));
};
return (
<SectionList
sections={sections}
keyExtractor={(item) => item.id}
renderSectionHeader={({ section }) => (
<Animated.View
entering={SlideInDown.springify()}
style={styles.sectionHeader}
>
<Text style={styles.sectionTitle}>{section.title}</Text>
</Animated.View>
)}
renderItem={({ item, index, section }) => (
<Animated.View
entering={FadeInRight.delay(index * 50).springify()}
exiting={FadeOutLeft.duration(200)}
layout={Layout.springify()}
style={styles.item}
>
<Text style={styles.itemName}>{item.name}</Text>
<Pressable onPress={() => removeItem(section.title, item.id)}>
<Text style={styles.removeButton}>×</Text>
</Pressable>
</Animated.View>
)}
contentContainerStyle={styles.container}
/>
);
}
const styles = StyleSheet.create({
container: {
padding: 16,
},
sectionHeader: {
backgroundColor: '#f5f5f5',
padding: 12,
marginTop: 16,
marginBottom: 8,
borderRadius: 8,
},
sectionTitle: {
fontSize: 14,
fontWeight: '600',
color: '#666',
textTransform: 'uppercase',
},
item: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: 'white',
padding: 16,
marginBottom: 8,
borderRadius: 12,
},
itemName: {
fontSize: 16,
},
removeButton: {
fontSize: 24,
color: '#999',
},
});
Hands-On Exercises
Exercise 1: Animated Notification Stack
Create a notification system where notifications stack, slide in, and can be dismissed.
Requirements:
- Notifications slide in from the top
- Multiple notifications stack with layout animation
- Swipe right to dismiss individual notifications
- Auto-dismiss after 5 seconds (optional)
Show Solution
import React, { useState, useEffect, useRef } from 'react';
import { StyleSheet, View, Text, Pressable, Dimensions } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
runOnJS,
SlideInUp,
SlideOutRight,
Layout,
} from 'react-native-reanimated';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
interface Notification {
id: string;
title: string;
message: string;
type: 'info' | 'success' | 'warning' | 'error';
}
function NotificationItem({
notification,
onDismiss
}: {
notification: Notification;
onDismiss: (id: string) => void;
}) {
const translateX = useSharedValue(0);
const timerRef = useRef<NodeJS.Timeout>();
useEffect(() => {
// Auto-dismiss after 5 seconds
timerRef.current = setTimeout(() => {
onDismiss(notification.id);
}, 5000);
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);
const panGesture = Gesture.Pan()
.onUpdate((event) => {
// Only allow right swipe
translateX.value = Math.max(0, event.translationX);
})
.onEnd((event) => {
if (translateX.value > 100 || event.velocityX > 500) {
translateX.value = withTiming(SCREEN_WIDTH, { duration: 200 }, () => {
runOnJS(onDismiss)(notification.id);
});
} else {
translateX.value = withSpring(0);
}
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
opacity: 1 - translateX.value / SCREEN_WIDTH,
}));
const getColors = () => {
switch (notification.type) {
case 'success': return { bg: '#34C759', icon: '✓' };
case 'error': return { bg: '#FF3B30', icon: '✕' };
case 'warning': return { bg: '#FF9500', icon: '⚠' };
default: return { bg: '#007AFF', icon: 'ℹ' };
}
};
const colors = getColors();
return (
<GestureDetector gesture={panGesture}>
<Animated.View
entering={SlideInUp.springify().damping(15)}
exiting={SlideOutRight.duration(200)}
layout={Layout.springify()}
style={[styles.notification, animatedStyle]}
>
<View style={[styles.iconContainer, { backgroundColor: colors.bg }]}>
<Text style={styles.icon}>{colors.icon}</Text>
</View>
<View style={styles.content}>
<Text style={styles.title}>{notification.title}</Text>
<Text style={styles.message}>{notification.message}</Text>
</View>
<Pressable
style={styles.closeButton}
onPress={() => onDismiss(notification.id)}
>
<Text style={styles.closeText}>×</Text>
</Pressable>
</Animated.View>
</GestureDetector>
);
}
function NotificationStack() {
const [notifications, setNotifications] = useState<Notification[]>([]);
const addNotification = (type: Notification['type']) => {
const newNotification: Notification = {
id: Date.now().toString(),
title: `${type.charAt(0).toUpperCase() + type.slice(1)} Notification`,
message: `This is a ${type} message that will auto-dismiss.`,
type,
};
setNotifications(prev => [newNotification, ...prev]);
};
const dismissNotification = (id: string) => {
setNotifications(prev => prev.filter(n => n.id !== id));
};
return (
<View style={styles.container}>
{/* Notification Stack */}
<View style={styles.stack}>
{notifications.map((notification) => (
<NotificationItem
key={notification.id}
notification={notification}
onDismiss={dismissNotification}
/>
))}
</View>
{/* Trigger Buttons */}
<View style={styles.buttons}>
<Pressable
style={[styles.button, { backgroundColor: '#007AFF' }]}
onPress={() => addNotification('info')}
>
<Text style={styles.buttonText}>Info</Text>
</Pressable>
<Pressable
style={[styles.button, { backgroundColor: '#34C759' }]}
onPress={() => addNotification('success')}
>
<Text style={styles.buttonText}>Success</Text>
</Pressable>
<Pressable
style={[styles.button, { backgroundColor: '#FF9500' }]}
onPress={() => addNotification('warning')}
>
<Text style={styles.buttonText}>Warning</Text>
</Pressable>
<Pressable
style={[styles.button, { backgroundColor: '#FF3B30' }]}
onPress={() => addNotification('error')}
>
<Text style={styles.buttonText}>Error</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
stack: {
position: 'absolute',
top: 50,
left: 16,
right: 16,
zIndex: 100,
gap: 8,
},
notification: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'white',
borderRadius: 12,
padding: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15,
shadowRadius: 8,
elevation: 5,
},
iconContainer: {
width: 36,
height: 36,
borderRadius: 18,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
icon: {
color: 'white',
fontSize: 18,
fontWeight: 'bold',
},
content: {
flex: 1,
},
title: {
fontSize: 16,
fontWeight: '600',
marginBottom: 2,
},
message: {
fontSize: 14,
color: '#666',
},
closeButton: {
padding: 8,
},
closeText: {
fontSize: 20,
color: '#999',
},
buttons: {
position: 'absolute',
bottom: 50,
left: 16,
right: 16,
flexDirection: 'row',
gap: 8,
},
button: {
flex: 1,
padding: 16,
borderRadius: 12,
alignItems: 'center',
},
buttonText: {
color: 'white',
fontWeight: '600',
},
});
Exercise 2: Animated Tab Bar
Build a custom animated tab bar with smooth indicator transitions.
Requirements:
- 4 tabs with icons
- Animated indicator slides between tabs
- Selected tab icon scales up
- Smooth spring animations
Show Solution
import React, { useState } from 'react';
import { StyleSheet, View, Text, Pressable, Dimensions } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
interpolateColor,
} from 'react-native-reanimated';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
interface Tab {
key: string;
icon: string;
label: string;
}
const tabs: Tab[] = [
{ key: 'home', icon: '🏠', label: 'Home' },
{ key: 'search', icon: '🔍', label: 'Search' },
{ key: 'notifications', icon: '🔔', label: 'Alerts' },
{ key: 'profile', icon: '👤', label: 'Profile' },
];
const TAB_WIDTH = SCREEN_WIDTH / tabs.length;
function AnimatedTabBar() {
const [activeIndex, setActiveIndex] = useState(0);
const indicatorPosition = useSharedValue(0);
const handleTabPress = (index: number) => {
setActiveIndex(index);
indicatorPosition.value = withSpring(index * TAB_WIDTH, {
damping: 15,
stiffness: 120,
});
};
const indicatorStyle = useAnimatedStyle(() => ({
transform: [{ translateX: indicatorPosition.value }],
}));
return (
<View style={styles.container}>
{/* Content Area */}
<View style={styles.content}>
<Text style={styles.contentText}>
{tabs[activeIndex].label} Screen
</Text>
</View>
{/* Tab Bar */}
<View style={styles.tabBar}>
{/* Animated Indicator */}
<Animated.View style={[styles.indicator, indicatorStyle]} />
{/* Tabs */}
{tabs.map((tab, index) => (
<TabItem
key={tab.key}
tab={tab}
index={index}
activeIndex={activeIndex}
onPress={() => handleTabPress(index)}
/>
))}
</View>
</View>
);
}
function TabItem({
tab,
index,
activeIndex,
onPress,
}: {
tab: Tab;
index: number;
activeIndex: number;
onPress: () => void;
}) {
const isActive = index === activeIndex;
const scale = useSharedValue(1);
React.useEffect(() => {
scale.value = withSpring(isActive ? 1.2 : 1, {
damping: 12,
stiffness: 150,
});
}, [isActive]);
const iconStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const labelStyle = useAnimatedStyle(() => ({
opacity: isActive ? 1 : 0.6,
transform: [{ scale: isActive ? 1 : 0.9 }],
}));
return (
<Pressable style={styles.tab} onPress={onPress}>
<Animated.Text style={[styles.tabIcon, iconStyle]}>
{tab.icon}
</Animated.Text>
<Animated.Text
style={[
styles.tabLabel,
labelStyle,
isActive && styles.activeLabel
]}
>
{tab.label}
</Animated.Text>
</Pressable>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
contentText: {
fontSize: 24,
fontWeight: '600',
},
tabBar: {
flexDirection: 'row',
height: 80,
backgroundColor: 'white',
borderTopWidth: 1,
borderTopColor: '#eee',
paddingBottom: 20,
},
indicator: {
position: 'absolute',
top: 0,
width: TAB_WIDTH,
height: 3,
backgroundColor: '#007AFF',
},
tab: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingTop: 8,
},
tabIcon: {
fontSize: 24,
marginBottom: 4,
},
tabLabel: {
fontSize: 12,
color: '#666',
},
activeLabel: {
color: '#007AFF',
fontWeight: '600',
},
});
Exercise 3: Animated Form Validation
Create a form with animated validation feedback.
Requirements:
- Input fields with animated border color on focus
- Shake animation on validation error
- Success checkmark animation when valid
- Error message slides in with layout animation
Show Solution
import React, { useState, useRef } from 'react';
import { StyleSheet, View, Text, TextInput, Pressable } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
withSequence,
interpolateColor,
FadeIn,
FadeOut,
Layout,
} from 'react-native-reanimated';
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
type ValidationState = 'idle' | 'valid' | 'invalid';
interface FormFieldProps {
label: string;
value: string;
onChangeText: (text: string) => void;
placeholder?: string;
validate: (value: string) => string | null;
secureTextEntry?: boolean;
}
function FormField({
label,
value,
onChangeText,
placeholder,
validate,
secureTextEntry,
}: FormFieldProps) {
const [focused, setFocused] = useState(false);
const [error, setError] = useState<string | null>(null);
const [validationState, setValidationState] = useState<ValidationState>('idle');
const borderProgress = useSharedValue(0);
const shakeX = useSharedValue(0);
const checkScale = useSharedValue(0);
const handleFocus = () => {
setFocused(true);
borderProgress.value = withTiming(0.5);
};
const handleBlur = () => {
setFocused(false);
const validationError = validate(value);
if (validationError) {
setError(validationError);
setValidationState('invalid');
borderProgress.value = withTiming(1);
checkScale.value = withTiming(0);
// Shake animation
shakeX.value = withSequence(
withTiming(-10, { duration: 50 }),
withTiming(10, { duration: 50 }),
withTiming(-10, { duration: 50 }),
withTiming(10, { duration: 50 }),
withTiming(0, { duration: 50 })
);
} else {
setError(null);
setValidationState('valid');
borderProgress.value = withTiming(0);
checkScale.value = withSpring(1, { damping: 8 });
}
};
const containerStyle = useAnimatedStyle(() => {
const borderColor = interpolateColor(
borderProgress.value,
[0, 0.5, 1],
['#E0E0E0', '#007AFF', '#FF3B30']
);
return {
borderColor,
transform: [{ translateX: shakeX.value }],
};
});
const checkStyle = useAnimatedStyle(() => ({
transform: [{ scale: checkScale.value }],
opacity: checkScale.value,
}));
return (
<Animated.View layout={Layout.springify()} style={styles.fieldContainer}>
<Text style={styles.label}>{label}</Text>
<Animated.View style={[styles.inputContainer, containerStyle]}>
<TextInput
style={styles.input}
value={value}
onChangeText={onChangeText}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder={placeholder}
placeholderTextColor="#999"
secureTextEntry={secureTextEntry}
/>
{validationState === 'valid' && (
<Animated.View style={[styles.checkmark, checkStyle]}>
<Text style={styles.checkmarkText}>✓</Text>
</Animated.View>
)}
</Animated.View>
{error && (
<Animated.Text
entering={FadeIn.duration(200)}
exiting={FadeOut.duration(200)}
style={styles.errorText}
>
{error}
</Animated.Text>
)}
</Animated.View>
);
}
function AnimatedForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const validateEmail = (value: string) => {
if (!value) return 'Email is required';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return 'Please enter a valid email';
}
return null;
};
const validatePassword = (value: string) => {
if (!value) return 'Password is required';
if (value.length < 8) return 'Password must be at least 8 characters';
return null;
};
const validateConfirmPassword = (value: string) => {
if (!value) return 'Please confirm your password';
if (value !== password) return 'Passwords do not match';
return null;
};
return (
<View style={styles.form}>
<Text style={styles.title}>Create Account</Text>
<FormField
label="Email"
value={email}
onChangeText={setEmail}
placeholder="you@example.com"
validate={validateEmail}
/>
<FormField
label="Password"
value={password}
onChangeText={setPassword}
placeholder="At least 8 characters"
validate={validatePassword}
secureTextEntry
/>
<FormField
label="Confirm Password"
value={confirmPassword}
onChangeText={setConfirmPassword}
placeholder="Re-enter your password"
validate={validateConfirmPassword}
secureTextEntry
/>
<Pressable style={styles.submitButton}>
<Text style={styles.submitText}>Sign Up</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
form: {
padding: 20,
},
title: {
fontSize: 28,
fontWeight: 'bold',
marginBottom: 24,
textAlign: 'center',
},
fieldContainer: {
marginBottom: 20,
},
label: {
fontSize: 14,
fontWeight: '600',
marginBottom: 8,
color: '#333',
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 2,
borderRadius: 12,
backgroundColor: 'white',
},
input: {
flex: 1,
padding: 16,
fontSize: 16,
},
checkmark: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: '#34C759',
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
checkmarkText: {
color: 'white',
fontWeight: 'bold',
},
errorText: {
color: '#FF3B30',
fontSize: 12,
marginTop: 6,
marginLeft: 4,
},
submitButton: {
backgroundColor: '#007AFF',
padding: 18,
borderRadius: 12,
alignItems: 'center',
marginTop: 20,
},
submitText: {
color: 'white',
fontSize: 18,
fontWeight: '600',
},
});
Summary
Layout animations bring polish and professionalism to your React Native apps by smoothly handling component lifecycle events. With Reanimated's declarative API, you can easily add entering, exiting, and layout transitions that would otherwise require complex manual animation code.
🎯 Key Takeaways
- Entering animations: Run when components mount (
FadeIn,SlideInUp,ZoomIn, etc.) - Exiting animations: Run when components unmount, delaying removal (
FadeOut,SlideOutLeft, etc.) - Layout transitions: Animate position/size changes within layouts (
Layout.springify()) - Keyframes: Define multi-step animations with precise timing control
- Customization: Chain modifiers like
.delay(),.duration(),.springify() - Lists: Combine with FlatList for animated add/remove/reorder
Layout Animation Quick Reference
| Animation Type | Usage | Common Options |
|---|---|---|
entering |
<Animated.View entering={FadeIn}> |
.delay(), .duration(), .springify() |
exiting |
<Animated.View exiting={FadeOut}> |
.delay(), .duration() |
layout |
<Animated.View layout={Layout}> |
.springify(), .damping(), .stiffness() |
| Keyframe | new Keyframe({ 0: {...}, 100: {...} }) |
.duration(), .delay() |
Animation Categories
| Category | Entering | Exiting |
|---|---|---|
| Fade | FadeIn, FadeInUp, FadeInDown |
FadeOut, FadeOutUp, FadeOutDown |
| Slide | SlideInLeft, SlideInRight, SlideInUp |
SlideOutLeft, SlideOutRight, SlideOutUp |
| Zoom | ZoomIn, ZoomInRotate |
ZoomOut, ZoomOutRotate |
| Bounce | BounceIn, BounceInUp |
BounceOut, BounceOutUp |
| Flip | FlipInXUp, FlipInYLeft |
FlipOutXUp, FlipOutYLeft |
This concludes Module 9 on Animations and Gestures. You now have the skills to create smooth, performant, and engaging animated interfaces that feel native and responsive.