├── .github └── demo.gif ├── .gitignore ├── App.tsx ├── README.md ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── icon.png └── splash.png ├── babel.config.js ├── package-lock.json ├── package.json ├── src ├── assets │ ├── image1.png │ ├── image2.png │ └── image3.png ├── components │ ├── Button.tsx │ └── Pagination.tsx ├── constants │ └── theme.ts ├── data │ └── screens.ts └── screens │ └── Onboarding.tsx └── tsconfig.json /.github/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/areasflavio/react-native-onboarding-screens/04ec6f651e8e48ffe45e16f07eae7880cc52946d/.github/demo.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # Temporary files created by Metro to check the health of the file watcher 17 | .metro-health-check* 18 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar } from 'expo-status-bar'; 2 | import React from 'react'; 3 | import { SafeAreaView, StyleSheet } from 'react-native'; 4 | 5 | import { Onboarding } from './src/screens/Onboarding'; 6 | 7 | export default function App() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | const styles = StyleSheet.create({ 18 | container: { 19 | flex: 1, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-Native-Onboarding-Screens 2 | 3 |

4 | A simple onboarding screens flow to an app. Built with React Native, Expo and Reanimated. 5 |

6 | 7 |

8 | GitHub top language 9 | 10 | GitHub language count 11 | 12 | 13 | GitHub last commit 14 | 15 |

16 | 17 |

18 | Features  |   19 | Technologies  |   20 | Installation 21 |

22 | 23 |

24 | demo 25 |

26 | 27 |
28 | 29 | # :star: Features 30 | 31 | [(Back to top)](#React-Native-Onboarding-Screens) 32 | 33 | Some key features are: 34 | 35 | - Gesture controls 36 | - Smooth animations 37 | 38 | The application is built using React Native with Expo framework. 39 | The entire codebase is written using Typescript. 40 | 41 |
42 | 43 | # :keyboard: Technologies 44 | 45 | [(Back to top)](#React-Native-Onboarding-Screens) 46 | 47 | This is what I used and learned with this project: 48 | 49 | - [x] React Native 50 | - [x] Expo 51 | - [x] Reanimated 52 | - [x] Typescript 53 | 54 |
55 | 56 | # :computer_mouse: Installation 57 | 58 | [(Back to top)](#React-Native-Onboarding-Screens) 59 | 60 | To use this project, first you need NodeJS installed in your device, 61 | then you can follow the commands below: 62 | 63 | ```bash 64 | # Clone this repository 65 | git clone https://github.com/areasflavio/react-native-onboarding-screens.git 66 | 67 | # Go into the repository 68 | cd react-native-onboarding-screens 69 | 70 | # Install dependencies for the application 71 | yarn install 72 | 73 | # To start the development server, run the following command 74 | yarn start 75 | 76 | # You start the emulator following the terminal instructions or: 77 | yarn start android # for android emulator 78 | 79 | yarn start ios # for ios emulator 80 | ``` 81 | 82 | # :man_technologist: Author 83 | 84 | [(Back to top)](#React-Native-Onboarding-Screens) 85 | 86 | Build by Flávio Arêas 👋 [Get in touch!](https://www.linkedin.com/in/areasflavio/) 87 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "rn-onboarding", 4 | "slug": "rn-onboarding", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "assetBundlePatterns": [ 15 | "**/*" 16 | ], 17 | "ios": { 18 | "supportsTablet": true 19 | }, 20 | "android": { 21 | "adaptiveIcon": { 22 | "foregroundImage": "./assets/adaptive-icon.png", 23 | "backgroundColor": "#ffffff" 24 | } 25 | }, 26 | "web": { 27 | "favicon": "./assets/favicon.png" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/areasflavio/react-native-onboarding-screens/04ec6f651e8e48ffe45e16f07eae7880cc52946d/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/areasflavio/react-native-onboarding-screens/04ec6f651e8e48ffe45e16f07eae7880cc52946d/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/areasflavio/react-native-onboarding-screens/04ec6f651e8e48ffe45e16f07eae7880cc52946d/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/areasflavio/react-native-onboarding-screens/04ec6f651e8e48ffe45e16f07eae7880cc52946d/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: ['react-native-reanimated/plugin'], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rn-onboarding", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "expo": "~48.0.15", 13 | "expo-status-bar": "~1.4.4", 14 | "react": "18.2.0", 15 | "react-native": "0.71.7", 16 | "react-native-reanimated": "~2.14.4" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.20.0", 20 | "@types/react": "~18.0.14", 21 | "typescript": "^4.9.4" 22 | }, 23 | "private": true 24 | } 25 | -------------------------------------------------------------------------------- /src/assets/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/areasflavio/react-native-onboarding-screens/04ec6f651e8e48ffe45e16f07eae7880cc52946d/src/assets/image1.png -------------------------------------------------------------------------------- /src/assets/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/areasflavio/react-native-onboarding-screens/04ec6f651e8e48ffe45e16f07eae7880cc52946d/src/assets/image2.png -------------------------------------------------------------------------------- /src/assets/image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/areasflavio/react-native-onboarding-screens/04ec6f651e8e48ffe45e16f07eae7880cc52946d/src/assets/image3.png -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { Feather } from '@expo/vector-icons'; 2 | import React, { RefObject } from 'react'; 3 | import { FlatList, Pressable, StyleSheet } from 'react-native'; 4 | import Animated, { 5 | SharedValue, 6 | useAnimatedStyle, 7 | withSpring, 8 | withTiming, 9 | } from 'react-native-reanimated'; 10 | 11 | import { theme } from '../constants/theme'; 12 | 13 | const AnimatedPressable = Animated.createAnimatedComponent(Pressable); 14 | 15 | type ButtonProps = { 16 | flatListRef: RefObject; 17 | flatListIndex: SharedValue; 18 | dataLength: number; 19 | }; 20 | 21 | export function Button({ 22 | dataLength, 23 | flatListIndex, 24 | flatListRef, 25 | }: ButtonProps) { 26 | const buttonAnimationStyle = useAnimatedStyle(() => { 27 | const isLastScreen = flatListIndex.value === dataLength - 1; 28 | return { 29 | width: isLastScreen ? withSpring(140) : withSpring(60), 30 | height: 60, 31 | }; 32 | }); 33 | 34 | const arrowAnimationStyle = useAnimatedStyle(() => { 35 | const isLastScreen = flatListIndex.value === dataLength - 1; 36 | return { 37 | opacity: isLastScreen ? withTiming(0) : withTiming(1), 38 | transform: [ 39 | { translateX: isLastScreen ? withTiming(100) : withTiming(0) }, 40 | ], 41 | }; 42 | }); 43 | 44 | const textAnimationStyle = useAnimatedStyle(() => { 45 | const isLastScreen = flatListIndex.value === dataLength - 1; 46 | return { 47 | opacity: isLastScreen ? withTiming(1) : withTiming(0), 48 | transform: [ 49 | { translateX: isLastScreen ? withTiming(0) : withTiming(-100) }, 50 | ], 51 | }; 52 | }); 53 | 54 | const handleNextScreen = () => { 55 | const isLastScreen = flatListIndex.value === dataLength - 1; 56 | if (!isLastScreen) { 57 | flatListRef.current?.scrollToIndex({ index: flatListIndex.value + 1 }); 58 | } 59 | }; 60 | 61 | return ( 62 | 66 | 67 | Get Started 68 | 69 | 70 | 71 | 76 | 77 | 78 | ); 79 | } 80 | 81 | const styles = StyleSheet.create({ 82 | container: { 83 | backgroundColor: theme.colors.backgroundHighlightColor, 84 | padding: 10, 85 | borderRadius: 100, 86 | alignItems: 'center', 87 | justifyContent: 'center', 88 | overflow: 'hidden', 89 | }, 90 | arrow: { 91 | position: 'absolute', 92 | }, 93 | text: { 94 | position: 'absolute', 95 | fontSize: 16, 96 | fontWeight: 'bold', 97 | color: theme.colors.textHighlightColor, 98 | }, 99 | }); 100 | -------------------------------------------------------------------------------- /src/components/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, View } from 'react-native'; 3 | import Animated, { 4 | Extrapolate, 5 | SharedValue, 6 | interpolate, 7 | useAnimatedStyle, 8 | } from 'react-native-reanimated'; 9 | 10 | import { theme } from '../constants/theme'; 11 | import { type Data } from '../data/screens'; 12 | 13 | type PaginationCompProps = { 14 | index: number; 15 | x: SharedValue; 16 | screenWidth: number; 17 | }; 18 | 19 | const PaginationComp = ({ index, x, screenWidth }: PaginationCompProps) => { 20 | const animatedDotStyle = useAnimatedStyle(() => { 21 | const widthAnimation = interpolate( 22 | x.value, 23 | [ 24 | (index - 1) * screenWidth, 25 | index * screenWidth, 26 | (index + 1) * screenWidth, 27 | ], 28 | [10, 20, 10], 29 | Extrapolate.CLAMP 30 | ); 31 | 32 | const opacityAnimation = interpolate( 33 | x.value, 34 | [ 35 | (index - 1) * screenWidth, 36 | index * screenWidth, 37 | (index + 1) * screenWidth, 38 | ], 39 | [0.5, 1, 0.5], 40 | Extrapolate.CLAMP 41 | ); 42 | 43 | return { 44 | width: widthAnimation, 45 | opacity: opacityAnimation, 46 | }; 47 | }); 48 | 49 | return ; 50 | }; 51 | 52 | type PaginationProps = { 53 | data: Data[]; 54 | x: SharedValue; 55 | screenWidth: number; 56 | }; 57 | 58 | export function Pagination({ data, screenWidth, x }: PaginationProps) { 59 | return ( 60 | 61 | {data.map((item, index) => ( 62 | 68 | ))} 69 | 70 | ); 71 | } 72 | 73 | const styles = StyleSheet.create({ 74 | container: { 75 | height: 40, 76 | flexDirection: 'row', 77 | alignItems: 'center', 78 | justifyContent: 'center', 79 | }, 80 | dots: { 81 | width: 10, 82 | height: 10, 83 | borderRadius: 5, 84 | backgroundColor: theme.colors.backgroundHighlightColor, 85 | marginHorizontal: 10, 86 | }, 87 | }); 88 | -------------------------------------------------------------------------------- /src/constants/theme.ts: -------------------------------------------------------------------------------- 1 | export const theme = { 2 | colors: { 3 | backgroundColor: '#f8e9b0', 4 | backgroundHighlightColor: '#f7a641', 5 | textColor: '#1b1b1b', 6 | textHighlightColor: '#f0f0f0', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/data/screens.ts: -------------------------------------------------------------------------------- 1 | export type Data = { 2 | id: number; 3 | image: any; 4 | title: string; 5 | text: string; 6 | }; 7 | 8 | export const data: Data[] = [ 9 | { 10 | id: 1, 11 | image: require('../assets/image1.png'), 12 | title: 'Lorem Ipsum', 13 | text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', 14 | }, 15 | { 16 | id: 2, 17 | image: require('../assets/image2.png'), 18 | title: 'Lorem Ipsum', 19 | text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', 20 | }, 21 | { 22 | id: 3, 23 | image: require('../assets/image3.png'), 24 | title: 'Lorem Ipsum', 25 | text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', 26 | }, 27 | ]; 28 | -------------------------------------------------------------------------------- /src/screens/Onboarding.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FlatList, 3 | StyleSheet, 4 | Text, 5 | useWindowDimensions, 6 | View, 7 | ViewToken, 8 | } from 'react-native'; 9 | import Animated, { 10 | Extrapolate, 11 | interpolate, 12 | SharedValue, 13 | useAnimatedRef, 14 | useAnimatedScrollHandler, 15 | useAnimatedStyle, 16 | useSharedValue, 17 | } from 'react-native-reanimated'; 18 | 19 | import { Button } from '../components/Button'; 20 | import { Pagination } from '../components/Pagination'; 21 | import { theme } from '../constants/theme'; 22 | import { data, type Data } from '../data/screens'; 23 | 24 | const RenderItem = ({ 25 | item, 26 | index, 27 | x, 28 | }: { 29 | item: Data; 30 | index: number; 31 | x: SharedValue; 32 | }) => { 33 | const { width: SCREEN_WIDTH } = useWindowDimensions(); 34 | 35 | const imageAnimatedStyle = useAnimatedStyle(() => { 36 | const opacityAnimation = interpolate( 37 | x.value, 38 | [ 39 | (index - 1) * SCREEN_WIDTH, 40 | index * SCREEN_WIDTH, 41 | (index + 1) * SCREEN_WIDTH, 42 | ], 43 | [0, 1, 0], 44 | Extrapolate.CLAMP 45 | ); 46 | 47 | const translateYAnimation = interpolate( 48 | x.value, 49 | [ 50 | (index - 1) * SCREEN_WIDTH, 51 | index * SCREEN_WIDTH, 52 | (index + 1) * SCREEN_WIDTH, 53 | ], 54 | [100, 0, 100], 55 | Extrapolate.CLAMP 56 | ); 57 | 58 | return { 59 | width: SCREEN_WIDTH * 0.8, 60 | height: SCREEN_WIDTH * 0.8, 61 | opacity: opacityAnimation, 62 | transform: [{ translateY: translateYAnimation }], 63 | }; 64 | }); 65 | 66 | const textAnimatedStyle = useAnimatedStyle(() => { 67 | const opacityAnimation = interpolate( 68 | x.value, 69 | [ 70 | (index - 1) * SCREEN_WIDTH, 71 | index * SCREEN_WIDTH, 72 | (index + 1) * SCREEN_WIDTH, 73 | ], 74 | [0, 1, 0], 75 | Extrapolate.CLAMP 76 | ); 77 | 78 | const translateYAnimation = interpolate( 79 | x.value, 80 | [ 81 | (index - 1) * SCREEN_WIDTH, 82 | index * SCREEN_WIDTH, 83 | (index + 1) * SCREEN_WIDTH, 84 | ], 85 | [100, 0, 100], 86 | Extrapolate.CLAMP 87 | ); 88 | 89 | return { 90 | opacity: opacityAnimation, 91 | transform: [{ translateY: translateYAnimation }], 92 | }; 93 | }); 94 | 95 | return ( 96 | 97 | 98 | 99 | 100 | {item.title} 101 | {item.text} 102 | 103 | 104 | ); 105 | }; 106 | 107 | export function Onboarding() { 108 | const { width: SCREEN_WIDTH } = useWindowDimensions(); 109 | const flatListRef = useAnimatedRef(); 110 | 111 | const flatListIndex = useSharedValue(0); 112 | const x = useSharedValue(0); 113 | 114 | const onViewableItemsChanged = ({ 115 | viewableItems, 116 | }: { 117 | viewableItems: Array; 118 | }) => { 119 | flatListIndex.value = viewableItems[0].index ?? 0; 120 | }; 121 | 122 | const onScroll = useAnimatedScrollHandler({ 123 | onScroll: (event) => { 124 | x.value = event.contentOffset.x; 125 | }, 126 | }); 127 | 128 | return ( 129 | 130 | String(item.id)} 134 | renderItem={({ item, index }) => ( 135 | 136 | )} 137 | onScroll={onScroll} 138 | scrollEventThrottle={16} 139 | horizontal 140 | showsHorizontalScrollIndicator={false} 141 | bounces={false} 142 | pagingEnabled 143 | onViewableItemsChanged={onViewableItemsChanged} 144 | /> 145 | 146 | 147 | 148 | 149 |