Building Animated Onboarding Screens with React Native Reanimated 3
Introduction
First impressions matter, especially in mobile applications. Onboarding screens are often the first interaction users have with your app, and they play a crucial role in user retention and engagement. With React Native Reanimated 3, you can create stunning, performant animated onboarding experiences that captivate users from the moment they open your app.
In this comprehensive guide, we'll build a complete onboarding flow with smooth transitions, gesture-based navigation, and beautiful animations using React Native Reanimated 3. By the end, you'll have a production-ready onboarding component that you can customize for your own applications.
What is Onboarding?
Onboarding is the process of introducing new users to your application. A well-designed onboarding experience should:
- Introduce key features: Highlight the main value propositions of your app
- Guide users: Show users how to navigate and use core functionality
- Build excitement: Create a positive first impression with engaging visuals and animations
- Reduce friction: Make it easy for users to get started quickly
- Set expectations: Clearly communicate what users can expect from your app
Why Use Reanimated 3?
React Native Reanimated 3 is the go-to library for creating performant animations in React Native. Here's why it's perfect for onboarding screens:
- 60 FPS animations: Runs on the native thread for buttery-smooth performance
- Declarative API: Easy-to-understand syntax with shared values and worklets
- Gesture handling: Seamlessly integrates with React Native Gesture Handler
- Layout animations: Built-in support for smooth layout transitions
- Web support: Works on React Native Web for cross-platform consistency
- New architecture ready: Fully compatible with the new React Native architecture
Prerequisites
Before we begin, ensure you have:
- React Native 0.68 or higher
- Node.js 14 or higher
- Basic understanding of React Native and TypeScript
- A React Native project set up and ready
Installing Dependencies
First, let's install the required packages:
npm install react-native-reanimated react-native-gesture-handler
# or
yarn add react-native-reanimated react-native-gesture-handler
For iOS, install CocoaPods dependencies:
cd ios && pod install && cd ..
Configuring Reanimated 3
Babel Configuration
Add the Reanimated plugin to your babel.config.js
:
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: [
// ... other plugins
'react-native-reanimated/plugin',
],
};
Important: The Reanimated plugin must be listed last in the plugins array.
Android Configuration
Reanimated 3 should work out of the box on Android, but ensure your MainApplication.java
(or MainApplication.kt
) has the proper setup:
import com.facebook.react.bridge.JSIModulePackage;
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
@Override
protected JSIModulePackage getJSIModulePackage() {
return new ReanimatedJSIModulePackage();
}
iOS Configuration
For iOS, no additional configuration is needed after running pod install
.
Entry Point Setup
Wrap your entry point with GestureHandlerRootView
:
// App.tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
{/* Your app content */}
</GestureHandlerRootView>
);
}
Building the Onboarding Screens
Step 1: Define Onboarding Data
First, let's create the data structure for our onboarding screens:
// data/onboardingData.ts
export interface OnboardingItem {
id: string;
title: string;
description: string;
image: any; // or use require() for local images
backgroundColor: string;
}
export const onboardingData: OnboardingItem[] = [
{
id: '1',
title: 'Welcome to Our App',
description: 'Discover amazing features that will transform the way you work and play.',
image: require('../assets/onboarding-1.png'),
backgroundColor: '#6366F1',
},
{
id: '2',
title: 'Stay Connected',
description: 'Connect with friends, family, and colleagues in real-time with our powerful messaging features.',
image: require('../assets/onboarding-2.png'),
backgroundColor: '#8B5CF6',
},
{
id: '3',
title: 'Boost Productivity',
description: 'Organize your tasks, set goals, and achieve more with our intuitive productivity tools.',
image: require('../assets/onboarding-3.png'),
backgroundColor: '#EC4899',
},
{
id: '4',
title: 'Get Started',
description: 'Ready to begin your journey? Let\'s get you set up and explore all the possibilities.',
image: require('../assets/onboarding-4.png'),
backgroundColor: '#F59E0B',
},
];
Step 2: Create the Onboarding Page Component
Now let's create an individual onboarding page with animations:
// components/OnboardingPage.tsx
import React from 'react';
import { View, Text, Image, StyleSheet, Dimensions } from 'react-native';
import Animated, {
useAnimatedStyle,
interpolate,
Extrapolate,
} from 'react-native-reanimated';
import { OnboardingItem } from '../data/onboardingData';
const { width, height } = Dimensions.get('window');
interface OnboardingPageProps {
item: OnboardingItem;
index: number;
scrollX: Animated.SharedValue<number>;
}
const OnboardingPage: React.FC<OnboardingPageProps> = ({ item, index, scrollX }) => {
// Animated style for the image
const imageAnimatedStyle = useAnimatedStyle(() => {
const inputRange = [
(index - 1) * width,
index * width,
(index + 1) * width,
];
const scale = interpolate(
scrollX.value,
inputRange,
[0, 1, 0],
Extrapolate.CLAMP
);
const translateY = interpolate(
scrollX.value,
inputRange,
[100, 0, 100],
Extrapolate.CLAMP
);
return {
transform: [{ scale }, { translateY }],
};
});
// Animated style for the text content
const textAnimatedStyle = useAnimatedStyle(() => {
const inputRange = [
(index - 1) * width,
index * width,
(index + 1) * width,
];
const opacity = interpolate(
scrollX.value,
inputRange,
[0, 1, 0],
Extrapolate.CLAMP
);
const translateY = interpolate(
scrollX.value,
inputRange,
[50, 0, 50],
Extrapolate.CLAMP
);
return {
opacity,
transform: [{ translateY }],
};
});
return (
<View style={[styles.container, { backgroundColor: item.backgroundColor }]}>
<Animated.View style={[styles.imageContainer, imageAnimatedStyle]}>
<Image source={item.image} style={styles.image} resizeMode="contain" />
</Animated.View>
<Animated.View style={[styles.textContainer, textAnimatedStyle]}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.description}>{item.description}</Text>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
container: {
width,
height,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 40,
},
imageContainer: {
flex: 0.6,
justifyContent: 'center',
alignItems: 'center',
},
image: {
width: width * 0.7,
height: width * 0.7,
},
textContainer: {
flex: 0.4,
alignItems: 'center',
paddingHorizontal: 20,
},
title: {
fontSize: 32,
fontWeight: 'bold',
color: '#FFFFFF',
textAlign: 'center',
marginBottom: 20,
},
description: {
fontSize: 16,
color: '#FFFFFF',
textAlign: 'center',
lineHeight: 24,
opacity: 0.9,
},
});
export default OnboardingPage;
Step 3: Create Pagination Dots
Let's add animated pagination dots to show progress:
// components/Pagination.tsx
import React from 'react';
import { View, StyleSheet, Dimensions } from 'react-native';
import Animated, {
useAnimatedStyle,
interpolate,
Extrapolate,
interpolateColor,
} from 'react-native-reanimated';
const { width } = Dimensions.get('window');
interface PaginationProps {
data: any[];
scrollX: Animated.SharedValue<number>;
}
const Pagination: React.FC<PaginationProps> = ({ data, scrollX }) => {
return (
<View style={styles.container}>
{data.map((_, index) => {
return (
<Dot key={index} index={index} scrollX={scrollX} />
);
})}
</View>
);
};
interface DotProps {
index: number;
scrollX: Animated.SharedValue<number>;
}
const Dot: React.FC<DotProps> = ({ index, scrollX }) => {
const animatedDotStyle = useAnimatedStyle(() => {
const inputRange = [
(index - 1) * width,
index * width,
(index + 1) * width,
];
const widthAnimation = interpolate(
scrollX.value,
inputRange,
[10, 30, 10],
Extrapolate.CLAMP
);
const opacityAnimation = interpolate(
scrollX.value,
inputRange,
[0.3, 1, 0.3],
Extrapolate.CLAMP
);
return {
width: widthAnimation,
opacity: opacityAnimation,
};
});
return <Animated.View style={[styles.dot, animatedDotStyle]} />;
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
bottom: 150,
},
dot: {
height: 10,
borderRadius: 5,
backgroundColor: '#FFFFFF',
marginHorizontal: 5,
},
});
export default Pagination;
Step 4: Create the Navigation Buttons
Add animated buttons for navigation:
// components/OnboardingButtons.tsx
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Dimensions } from 'react-native';
import Animated, {
useAnimatedStyle,
withSpring,
withTiming,
interpolate,
Extrapolate,
} from 'react-native-reanimated';
const { width } = Dimensions.get('window');
const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);
interface OnboardingButtonsProps {
dataLength: number;
currentIndex: Animated.SharedValue<number>;
scrollX: Animated.SharedValue<number>;
flatListRef: React.RefObject<any>;
onComplete: () => void;
}
const OnboardingButtons: React.FC<OnboardingButtonsProps> = ({
dataLength,
currentIndex,
scrollX,
flatListRef,
onComplete,
}) => {
const handleNext = () => {
if (currentIndex.value < dataLength - 1) {
flatListRef.current?.scrollToIndex({
index: currentIndex.value + 1,
animated: true,
});
} else {
onComplete();
}
};
const handleSkip = () => {
onComplete();
};
// Animated style for skip button
const skipButtonStyle = useAnimatedStyle(() => {
const opacity = interpolate(
scrollX.value,
[(dataLength - 2) * width, (dataLength - 1) * width],
[1, 0],
Extrapolate.CLAMP
);
return {
opacity,
};
});
// Animated style for next button
const nextButtonStyle = useAnimatedStyle(() => {
const inputRange = [
(dataLength - 2) * width,
(dataLength - 1) * width,
];
const scale = interpolate(
scrollX.value,
inputRange,
[1, 1.2],
Extrapolate.CLAMP
);
const backgroundColor = interpolate(
scrollX.value,
inputRange,
[0, 1],
Extrapolate.CLAMP
);
return {
transform: [{ scale }],
backgroundColor: backgroundColor > 0.5 ? '#10B981' : '#FFFFFF',
};
});
// Animated style for button text
const buttonTextStyle = useAnimatedStyle(() => {
const inputRange = [
(dataLength - 2) * width,
(dataLength - 1) * width,
];
const progress = interpolate(
scrollX.value,
inputRange,
[0, 1],
Extrapolate.CLAMP
);
return {
color: progress > 0.5 ? '#FFFFFF' : '#000000',
};
});
return (
<View style={styles.container}>
<AnimatedTouchable
style={[styles.skipButton, skipButtonStyle]}
onPress={handleSkip}
>
<Text style={styles.skipText}>Skip</Text>
</AnimatedTouchable>
<AnimatedTouchable
style={[styles.nextButton, nextButtonStyle]}
onPress={handleNext}
>
<Animated.Text style={[styles.nextText, buttonTextStyle]}>
{currentIndex.value === dataLength - 1 ? 'Get Started' : 'Next'}
</Animated.Text>
</AnimatedTouchable>
</View>
);
};
const styles = StyleSheet.create({
container: {
position: 'absolute',
bottom: 50,
width: width - 80,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 40,
},
skipButton: {
paddingHorizontal: 20,
paddingVertical: 10,
},
skipText: {
fontSize: 16,
color: '#FFFFFF',
fontWeight: '600',
},
nextButton: {
backgroundColor: '#FFFFFF',
paddingHorizontal: 30,
paddingVertical: 15,
borderRadius: 25,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 5,
elevation: 5,
},
nextText: {
fontSize: 16,
fontWeight: 'bold',
},
});
export default OnboardingButtons;
Step 5: Create the Main Onboarding Component
Now let's bring it all together:
// components/Onboarding.tsx
import React, { useRef } from 'react';
import { View, FlatList, StyleSheet, ViewToken } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedScrollHandler,
useAnimatedRef,
} from 'react-native-reanimated';
import { onboardingData, OnboardingItem } from '../data/onboardingData';
import OnboardingPage from './OnboardingPage';
import Pagination from './Pagination';
import OnboardingButtons from './OnboardingButtons';
const AnimatedFlatList = Animated.createAnimatedComponent(
FlatList<OnboardingItem>
);
interface OnboardingProps {
onComplete: () => void;
}
const Onboarding: React.FC<OnboardingProps> = ({ onComplete }) => {
const flatListRef = useAnimatedRef<FlatList<OnboardingItem>>();
const scrollX = useSharedValue(0);
const currentIndex = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollX.value = event.contentOffset.x;
currentIndex.value = Math.round(event.contentOffset.x / event.layoutMeasurement.width);
},
});
const onViewableItemsChanged = useRef(
({ viewableItems }: { viewableItems: ViewToken[] }) => {
if (viewableItems[0]?.index !== null && viewableItems[0]?.index !== undefined) {
currentIndex.value = viewableItems[0].index;
}
}
).current;
const viewabilityConfig = useRef({
itemVisiblePercentThreshold: 50,
}).current;
const renderItem = ({ item, index }: { item: OnboardingItem; index: number }) => {
return <OnboardingPage item={item} index={index} scrollX={scrollX} />;
};
return (
<View style={styles.container}>
<AnimatedFlatList
ref={flatListRef}
data={onboardingData}
renderItem={renderItem}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
onScroll={scrollHandler}
scrollEventThrottle={16}
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
keyExtractor={(item) => item.id}
/>
<Pagination data={onboardingData} scrollX={scrollX} />
<OnboardingButtons
dataLength={onboardingData.length}
currentIndex={currentIndex}
scrollX={scrollX}
flatListRef={flatListRef}
onComplete={onComplete}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
},
});
export default Onboarding;
Adding Gesture-Based Navigation
Let's enhance the experience with swipe gestures using React Native Gesture Handler:
// components/GestureOnboarding.tsx
import React, { useRef } from 'react';
import { View, FlatList, StyleSheet, Dimensions } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedScrollHandler,
useAnimatedRef,
useAnimatedGestureHandler,
withSpring,
runOnJS,
} from 'react-native-reanimated';
import {
PanGestureHandler,
PanGestureHandlerGestureEvent,
} from 'react-native-gesture-handler';
import { onboardingData, OnboardingItem } from '../data/onboardingData';
import OnboardingPage from './OnboardingPage';
import Pagination from './Pagination';
const { width } = Dimensions.get('window');
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList<OnboardingItem>);
interface GestureOnboardingProps {
onComplete: () => void;
}
const GestureOnboarding: React.FC<GestureOnboardingProps> = ({ onComplete }) => {
const flatListRef = useAnimatedRef<FlatList<OnboardingItem>>();
const scrollX = useSharedValue(0);
const currentIndex = useSharedValue(0);
const translateX = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollX.value = event.contentOffset.x;
},
});
const scrollToIndex = (index: number) => {
flatListRef.current?.scrollToIndex({ index, animated: true });
};
const gestureHandler = useAnimatedGestureHandler<
PanGestureHandlerGestureEvent,
{ startX: number }
>({
onStart: (_, context) => {
context.startX = translateX.value;
},
onActive: (event, context) => {
translateX.value = context.startX + event.translationX;
},
onEnd: (event) => {
const shouldMoveNext = event.velocityX < -500 || event.translationX < -width / 3;
const shouldMovePrev = event.velocityX > 500 || event.translationX > width / 3;
if (shouldMoveNext && currentIndex.value < onboardingData.length - 1) {
currentIndex.value += 1;
runOnJS(scrollToIndex)(currentIndex.value);
} else if (shouldMovePrev && currentIndex.value > 0) {
currentIndex.value -= 1;
runOnJS(scrollToIndex)(currentIndex.value);
} else if (shouldMoveNext && currentIndex.value === onboardingData.length - 1) {
runOnJS(onComplete)();
}
translateX.value = withSpring(0);
},
});
const renderItem = ({ item, index }: { item: OnboardingItem; index: number }) => {
return <OnboardingPage item={item} index={index} scrollX={scrollX} />;
};
return (
<View style={styles.container}>
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View style={{ flex: 1 }}>
<AnimatedFlatList
ref={flatListRef}
data={onboardingData}
renderItem={renderItem}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
onScroll={scrollHandler}
scrollEventThrottle={16}
keyExtractor={(item) => item.id}
/>
</Animated.View>
</PanGestureHandler>
<Pagination data={onboardingData} scrollX={scrollX} />
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
},
});
export default GestureOnboarding;
Advanced: Adding Complex Animations
Let's add some more advanced animations for a polished experience:
Parallax Background Animation
// components/ParallaxBackground.tsx
import React from 'react';
import { View, StyleSheet, Dimensions } from 'react-native';
import Animated, {
useAnimatedStyle,
interpolate,
Extrapolate,
} from 'react-native-reanimated';
import Svg, { Circle, Path } from 'react-native-svg';
const { width, height } = Dimensions.get('window');
interface ParallaxBackgroundProps {
scrollX: Animated.SharedValue<number>;
backgroundColor: string;
}
const ParallaxBackground: React.FC<ParallaxBackgroundProps> = ({
scrollX,
backgroundColor,
}) => {
// Animated style for first layer
const layer1Style = useAnimatedStyle(() => {
const translateX = interpolate(
scrollX.value,
[0, width],
[0, -width * 0.3],
Extrapolate.CLAMP
);
return {
transform: [{ translateX }],
};
});
// Animated style for second layer
const layer2Style = useAnimatedStyle(() => {
const translateX = interpolate(
scrollX.value,
[0, width],
[0, -width * 0.5],
Extrapolate.CLAMP
);
return {
transform: [{ translateX }],
};
});
return (
<View style={[styles.container, { backgroundColor }]}>
<Animated.View style={[styles.layer, layer1Style]}>
<Svg width={width * 2} height={height} style={styles.svg}>
<Circle cx={width * 0.3} cy={height * 0.2} r="100" fill="rgba(255,255,255,0.1)" />
<Circle cx={width * 0.7} cy={height * 0.4} r="150" fill="rgba(255,255,255,0.05)" />
</Svg>
</Animated.View>
<Animated.View style={[styles.layer, layer2Style]}>
<Svg width={width * 2} height={height} style={styles.svg}>
<Circle cx={width * 0.5} cy={height * 0.6} r="120" fill="rgba(255,255,255,0.08)" />
<Circle cx={width * 0.9} cy={height * 0.8} r="80" fill="rgba(255,255,255,0.12)" />
</Svg>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
overflow: 'hidden',
},
layer: {
position: 'absolute',
width: width * 2,
height,
},
svg: {
position: 'absolute',
},
});
export default ParallaxBackground;
Animated Progress Bar
// components/AnimatedProgressBar.tsx
import React from 'react';
import { View, StyleSheet, Dimensions } from 'react-native';
import Animated, {
useAnimatedStyle,
interpolate,
Extrapolate,
} from 'react-native-reanimated';
const { width } = Dimensions.get('window');
interface AnimatedProgressBarProps {
dataLength: number;
scrollX: Animated.SharedValue<number>;
}
const AnimatedProgressBar: React.FC<AnimatedProgressBarProps> = ({
dataLength,
scrollX,
}) => {
const progressStyle = useAnimatedStyle(() => {
const progressWidth = interpolate(
scrollX.value,
[0, (dataLength - 1) * width],
[0, width - 80],
Extrapolate.CLAMP
);
return {
width: progressWidth,
};
});
return (
<View style={styles.container}>
<View style={styles.progressBarBackground}>
<Animated.View style={[styles.progressBar, progressStyle]} />
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
position: 'absolute',
top: 60,
width: width - 80,
alignSelf: 'center',
},
progressBarBackground: {
height: 4,
backgroundColor: 'rgba(255, 255, 255, 0.3)',
borderRadius: 2,
overflow: 'hidden',
},
progressBar: {
height: '100%',
backgroundColor: '#FFFFFF',
borderRadius: 2,
},
});
export default AnimatedProgressBar;
Persisting Onboarding State
Users should only see the onboarding flow once. Let's implement persistence:
// utils/onboardingStorage.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
const ONBOARDING_KEY = '@onboarding_completed';
export const setOnboardingCompleted = async (): Promise<void> => {
try {
await AsyncStorage.setItem(ONBOARDING_KEY, 'true');
} catch (error) {
console.error('Error saving onboarding state:', error);
}
};
export const hasCompletedOnboarding = async (): Promise<boolean> => {
try {
const value = await AsyncStorage.getItem(ONBOARDING_KEY);
return value === 'true';
} catch (error) {
console.error('Error reading onboarding state:', error);
return false;
}
};
export const resetOnboarding = async (): Promise<void> => {
try {
await AsyncStorage.removeItem(ONBOARDING_KEY);
} catch (error) {
console.error('Error resetting onboarding state:', error);
}
};
Install AsyncStorage first:
npm install @react-native-async-storage/async-storage
# or
yarn add @react-native-async-storage/async-storage
Then use it in your App component:
// App.tsx
import React, { useEffect, useState } from 'react';
import { View, ActivityIndicator } from 'react-native';
import Onboarding from './components/Onboarding';
import HomeScreen from './screens/HomeScreen';
import {
hasCompletedOnboarding,
setOnboardingCompleted,
} from './utils/onboardingStorage';
const App: React.FC = () => {
const [isLoading, setIsLoading] = useState(true);
const [showOnboarding, setShowOnboarding] = useState(false);
useEffect(() => {
checkOnboardingStatus();
}, []);
const checkOnboardingStatus = async () => {
const completed = await hasCompletedOnboarding();
setShowOnboarding(!completed);
setIsLoading(false);
};
const handleOnboardingComplete = async () => {
await setOnboardingCompleted();
setShowOnboarding(false);
};
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
</View>
);
}
if (showOnboarding) {
return <Onboarding onComplete={handleOnboardingComplete} />;
}
return <HomeScreen />;
};
export default App;
Best Practices
Performance Optimization
- Use worklets: Keep animations on the UI thread
const animatedStyle = useAnimatedStyle(() => {
'worklet';
// Animation logic here
return { transform: [{ translateX: scrollX.value }] };
});
- Optimize images: Use appropriately sized images and consider using
FastImage
:
npm install react-native-fast-image
- Memoize components: Use
React.memo
for onboarding pages:
export default React.memo(OnboardingPage);
Design Best Practices
- Keep it short: 3-5 screens maximum
- Focus on value: Highlight key benefits, not features
- Use clear CTAs: Make action buttons obvious
- Allow skipping: Don't force users to complete onboarding
- Maintain consistency: Use consistent colors, fonts, and spacing
- Test on real devices: Animations may behave differently on various devices
Accessibility
Add accessibility support to your onboarding:
<View
accessible={true}
accessibilityRole="text"
accessibilityLabel={`Onboarding screen ${index + 1} of ${dataLength}. ${item.title}. ${item.description}`}
>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.description}>{item.description}</Text>
</View>
Testing Your Onboarding
Unit Testing
// __tests__/Onboarding.test.tsx
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import Onboarding from '../components/Onboarding';
describe('Onboarding', () => {
it('renders correctly', () => {
const { getByText } = render(<Onboarding onComplete={() => {}} />);
expect(getByText('Welcome to Our App')).toBeTruthy();
});
it('calls onComplete when finishing onboarding', () => {
const mockOnComplete = jest.fn();
const { getByText } = render(<Onboarding onComplete={mockOnComplete} />);
// Simulate scrolling to last page and pressing "Get Started"
// This would require additional setup for Reanimated testing
fireEvent.press(getByText('Get Started'));
expect(mockOnComplete).toHaveBeenCalled();
});
it('allows skipping onboarding', () => {
const mockOnComplete = jest.fn();
const { getByText } = render(<Onboarding onComplete={mockOnComplete} />);
fireEvent.press(getByText('Skip'));
expect(mockOnComplete).toHaveBeenCalled();
});
});
E2E Testing with Detox
// e2e/onboarding.e2e.ts
describe('Onboarding Flow', () => {
beforeAll(async () => {
await device.launchApp({ newInstance: true });
});
it('should complete onboarding flow', async () => {
await expect(element(by.text('Welcome to Our App'))).toBeVisible();
// Swipe through onboarding screens
await element(by.id('onboarding-flatlist')).swipe('left');
await expect(element(by.text('Stay Connected'))).toBeVisible();
await element(by.id('onboarding-flatlist')).swipe('left');
await expect(element(by.text('Boost Productivity'))).toBeVisible();
await element(by.id('onboarding-flatlist')).swipe('left');
await expect(element(by.text('Get Started'))).toBeVisible();
// Complete onboarding
await element(by.text('Get Started')).tap();
await expect(element(by.id('home-screen'))).toBeVisible();
});
it('should skip onboarding', async () => {
await expect(element(by.text('Welcome to Our App'))).toBeVisible();
await element(by.text('Skip')).tap();
await expect(element(by.id('home-screen'))).toBeVisible();
});
});
Common Issues and Solutions
Issue 1: Animations are laggy
Solution: Ensure the Reanimated plugin is properly configured in babel.config.js
and rebuild your app:
# Clear cache and rebuild
npm start -- --reset-cache
# iOS
cd ios && pod install && cd .. && npm run ios
# Android
npm run android
Issue 2: FlatList not scrolling smoothly
Solution: Use scrollEventThrottle={16}
and ensure pagingEnabled={true}
:
<FlatList
scrollEventThrottle={16}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
// ... other props
/>
Issue 3: Images loading slowly
Solution: Optimize images and preload them:
import { Image } from 'react-native';
import { onboardingData } from './data/onboardingData';
// Preload images
useEffect(() => {
onboardingData.forEach((item) => {
Image.prefetch(item.image);
});
}, []);
Issue 4: Android animations not smooth
Solution: Enable hardware acceleration in AndroidManifest.xml
:
<application
android:hardwareAccelerated="true"
...
>
Advanced Customization
Custom Page Transitions
Create unique transition effects for each page:
const customTransitionStyle = useAnimatedStyle(() => {
const inputRange = [(index - 1) * width, index * width, (index + 1) * width];
// Rotation effect
const rotate = interpolate(
scrollX.value,
inputRange,
[45, 0, -45],
Extrapolate.CLAMP
);
// 3D perspective effect
const rotateY = interpolate(
scrollX.value,
inputRange,
[90, 0, -90],
Extrapolate.CLAMP
);
return {
transform: [
{ perspective: 1000 },
{ rotateY: `${rotateY}deg` },
{ rotate: `${rotate}deg` },
],
};
});
Dynamic Theme Colors
Interpolate colors based on scroll position:
import { interpolateColor } from 'react-native-reanimated';
const backgroundColorStyle = useAnimatedStyle(() => {
const backgroundColor = interpolateColor(
scrollX.value,
[0, width, width * 2, width * 3],
['#6366F1', '#8B5CF6', '#EC4899', '#F59E0B']
);
return { backgroundColor };
});
Performance Monitoring
Monitor your onboarding performance:
import { PerformanceObserver, PerformanceEntry } from 'react-native-performance';
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
console.log(`${entry.name}: ${entry.duration}ms`);
});
});
observer.observe({ entryTypes: ['measure'] });
// Measure onboarding load time
performance.mark('onboarding-start');
// ... onboarding component mounts
performance.mark('onboarding-end');
performance.measure('onboarding-load', 'onboarding-start', 'onboarding-end');
Conclusion
Congratulations! You've built a beautiful, performant onboarding experience with React Native Reanimated 3. In this comprehensive guide, we covered:
- Setting up Reanimated 3 and Gesture Handler
- Creating animated onboarding pages with parallax effects
- Implementing pagination indicators with smooth transitions
- Adding gesture-based navigation
- Persisting onboarding completion state
- Performance optimization techniques
- Testing strategies
- Common issues and solutions
- Advanced customization options
Remember that onboarding is your app's first impression. Keep it concise, engaging, and valuable. Focus on demonstrating the core benefits of your app rather than overwhelming users with features.
The animation techniques you've learned here can be applied to many other parts of your React Native applications. Reanimated 3 provides incredible performance and flexibility for creating engaging user experiences.
Happy coding, and may your users love their onboarding experience! 🚀