├── .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 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Features |
19 | Technologies |
20 | Installation
21 |
22 |
23 |
24 |
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 |
154 |
155 |
156 | );
157 | }
158 |
159 | const styles = StyleSheet.create({
160 | container: {
161 | flex: 1,
162 | backgroundColor: theme.colors.backgroundColor,
163 | },
164 | itemContainer: {
165 | flex: 1,
166 | backgroundColor: theme.colors.backgroundColor,
167 | alignItems: 'center',
168 | justifyContent: 'space-around',
169 | },
170 | itemTitle: {
171 | color: theme.colors.textColor,
172 | fontSize: 22,
173 | fontWeight: 'bold',
174 | textAlign: 'center',
175 | marginBottom: 10,
176 | },
177 | itemText: {
178 | color: theme.colors.textColor,
179 | textAlign: 'center',
180 | lineHeight: 20,
181 | marginHorizontal: 30,
182 | },
183 | footerContainer: {
184 | flexDirection: 'row',
185 | alignItems: 'center',
186 | justifyContent: 'space-between',
187 | margin: 20,
188 | },
189 | });
190 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------