Step-by-Step Guide: Implementing Snap Horizontal Scroll in React Native
Christopher Alves
Posted on February 21, 2024
The focus of this article is to assist you in creating a page with a horizontally scrollable component in snap style and a header that reflects the changes between the steps.
This structure is very useful for building wizard forms. It's a very useful form type where the user is guided to fill out the form through steps.
Article in Portuguese(🇧🇷) by clicking here
Stacks
In this project, we will use the following technologies:
- React Native and Expo - For creating and sharing the application, running natively on Android and iOS.
- React Native Responsive Font Size - For standardizing font sizes on Android and iOS devices.
Structure
Since the purpose of this article is to build content with horizontally scrollable snap-style steps, I won't focus on styling other components.
The initial structure has been divided into 2 parts:
- Header
- Content Container
Header
Responsible for indicating which steps have been completed and which ones are yet to be finished.
Content Container
Responsible for wrapping up all the content steps and horizontal snap scrolling.
Note: For standardizing font sizes on various devices, we will use the react-native-fontsize
library.
I created a utils file that contains the logic for standardizing font sizes.
// src/utils/normalize-font.ts
import { Dimensions } from "react-native";
import { RFValue } from "react-native-responsive-fontsize";
export const normalizeFont = (value: number) => {
const { height } = Dimensions.get("window");
const valueNormalized = RFValue(value, height);
return valueNormalized;
};
Initial Structure
// src/app/index.tsx
import { View, Text } from "react-native";
import { normalizeFont } from "../utils/normalize-font";
export default function Home() {
return (
// Screen Container
<View
style={{
flexGrow: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
}}
>
{/* Sign Up Container */}
<View
style={{
width: "100%",
borderRadius: 2,
backgroundColor: "2d0381",
}}
>
{/* Header */}
<View
style={{
backgroundColor: "#f0000099",
alignItems: "center",
justifyContent: "center",
padding: 40,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Header
</Text>
</View>
{/* Content Container */}
<View
style={{
backgroundColor: "green",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
width: "100%",
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Content
</Text>
</View>
</View>
</View>
);
}
Building the Content Container
Let's leave the Header for last since its behavior depends on the user's action in the content container.
Next steps are:
- Change the View of the Content Container to ScrollView, as we need horizontal scroll and add steps inside the Content Container.
- Adjust the width of the steps inside the Content Container.
- Enable paging.
- Navigation between steps.
Changing View of the Content Container to ScrollView
// src/app/index.tsx
import { View, Text, ScrollView } from "react-native";
import { normalizeFont } from "../utils/normalize-font";
export default function Home() {
return (
// Screen Container
<View
style={{
flexGrow: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
}}
>
{/* Sign Up Container */}
<View
style={{
width: "100%",
borderRadius: 2,
backgroundColor: "2d0381",
}}
>
{/* Header */}
<View
style={{
backgroundColor: "#f0000099",
alignItems: "center",
justifyContent: "center",
padding: 40,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Header
</Text>
</View>
{/* Content Container */}
<ScrollView
style={{
flexGrow: 0,
alignContent: "center",
}}
contentContainerStyle={{
flexGrow: 1,
position: "relative",
}}
horizontal
>
<View
style={{
width: "100%",
backgroundColor: "gray",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Welcome (Step 1)
</Text>
</View>
<View
style={{
width: "100%",
backgroundColor: "green",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Account Information (Step 2)
</Text>
</View>
<View
style={{
width: "100%",
backgroundColor: "orange",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Personal Information (Step 3)
</Text>
</View>
<View
style={{
width: "100%",
backgroundColor: "red",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Complete (Step 4)
</Text>
</View>
</ScrollView>
</View>
</View>
);
}
With the above structure, it's not possible to navigate horizontally because all steps have width = 100%, taking up all available space, hindering scrolling. To enable scrolling, we need to set the width of the steps equal to the available space of the content container, but without using 100% width, for example:
- If the content container has a width = 370px, then all steps inside it must also have this width.
Adjusting the width of steps inside the content container
To have dynamic width for the steps of the content container, we will assign to each step's width the size of the device screen, minus any horizontal gap, padding, or margin that exists.
To get the device's width, we will use Dimensions
, a method that comes from react-native.
We will also assign the variable screenPadding
the screen's padding, which is 20.
In the variable stepFormWidth
, we assign the result of the calculation of the screen's width minus the screen's padding. Since the screen's padding is 20 for the left side and 20 for the right side, we can multiply the variable screenPadding
by 2.
import { Dimensions } from "react-native";
const { width } = Dimensions.get("window");
const screenPadding = 20;
const stepFormWidth = width - screenPadding * 2;
Thus, the file looks like this:
// src/app/index.tsx
import { View, Text, Dimensions, ScrollView } from "react-native";
import { normalizeFont } from "../utils/normalize-font";
const { width } = Dimensions.get("window");
export default function Home() {
const screenPadding = 20;
const stepFormWidth = width - screenPadding * 2;
return (
// Screen Container
<View
style={{
flexGrow: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
}}
>
{/* Sign Up Container */}
<View
style={{
width: "100%",
borderRadius: 2,
backgroundColor: "2d0381",
}}
>
{/* Header */}
<View
style={{
backgroundColor: "#f0000099",
alignItems: "center",
justifyContent: "center",
padding: 40,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Header
</Text>
</View>
{/* Content Container */}
<ScrollView
style={{
flexGrow: 0,
alignContent: "center",
}}
contentContainerStyle={{
flexGrow: 1,
position: "relative",
}}
horizontal
>
<View
style={{
width: stepFormWidth,
backgroundColor: "gray",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Welcome (Step 1)
</Text>
</View>
<View
style={{
width: stepFormWidth,
backgroundColor: "green",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Account Information (Step 2)
</Text>
</View>
<View
style={{
width: stepFormWidth,
backgroundColor: "orange",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Personal Information (Step 3)
</Text>
</View>
<View
style={{
width: stepFormWidth,
backgroundColor: "red",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Complete (Step 4)
</Text>
</View>
</ScrollView>
</View>
</View>
);
}
Now that the Steps already have a specific size, we can make horizontal scrolling.
Enabling Paging
The way the horizontal scroll behaves is not the most ideal to use in a wizard form component, so we will make the scroll behave in a snap style, where scrolling just a little will move it to the center of the next step.
The ScrollView has the pagingEnabled
property that allows enabling this snap-style behavior.
// ...
<ScrollView
// ...
pagingEnabled
>
// ...
Navigation between steps.
Adding buttons for navigation between steps.
Now, within each step, we'll add buttons that allow moving forward or backward between steps.
// src/app/index.tsx
import { View, Text, Dimensions, Button, ScrollView } from "react-native";
import { normalizeFont } from "../utils/normalize-font";
const { width } = Dimensions.get("window");
export default function Home() {
const screenPadding = 20;
const stepFormWidth = width - screenPadding * 2;
return (
// Screen Container
<View
style={{
flexGrow: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
}}
>
{/* Sign Up Container */}
<View
style={{
width: "100%",
borderRadius: 2,
backgroundColor: "2d0381",
}}
>
{/* Header */}
<View
style={{
backgroundColor: "#f0000099",
alignItems: "center",
justifyContent: "center",
padding: 40,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Header
</Text>
</View>
{/* Content Container */}
<ScrollView
style={{
flexGrow: 0,
alignContent: "center",
}}
contentContainerStyle={{
flexGrow: 1,
position: "relative",
}}
horizontal
pagingEnabled
>
<View
style={{
width: stepFormWidth,
backgroundColor: "gray",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Welcome (Step 1)
</Text>
<Button
title="continue"
color="#000"
onPress={() => {
console.log("go to next step");
}}
/>
</View>
<View
style={{
width: stepFormWidth,
backgroundColor: "green",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Button
title="previous step"
color="#000"
onPress={() => {
console.log("go to previous step");
}}
/>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Account Information (Step 2)
</Text>
<Button
title="next step"
color="#000"
onPress={() => {
console.log("go to next step");
}}
/>
</View>
<View
style={{
width: stepFormWidth,
backgroundColor: "orange",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Button
title="previous step"
color="#000"
onPress={() => {
console.log("go to previous step");
}}
/>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Personal Information (Step 3)
</Text>
<Button
title="next step"
color="#000"
onPress={() => {
console.log("go to next step");
}}
/>
</View>
<View
style={{
width: stepFormWidth,
backgroundColor: "red",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Complete (Step 4)
</Text>
</View>
</ScrollView>
</View>
</View>
);
}
- Welcome (Step 1) only has the possibility to proceed to the next step.
- Account Information (Step 2) has the possibility to proceed and return.
- Personal Information (Step 3) has the possibility to proceed and return.
- Complete (Step 4) will be the completion screen, so there's no way to proceed or return.
Logic for navigation between steps.
Creating the Ref
To have navigation between steps, we need a reference to the ScrollView and use the scrollTo method, which allows us to scroll to a specific point on the X or Y axis.
import { useRef } from "react";
const scrollRef = useRef<ScrollView>(null);
// ...
<ScrollView
ref={scrollRef}
// ...
>
// ...
With the reference to the scrollview component, we can use scrollTo to navigate horizontally to the left or right.
Navigate to the next step
To navigate to the next step, we need to tell our scrollview component to navigate along the X axis to a specific value, and since our steps have a size equal to the device's screen, reducing only the horizontal padding, we can use this information to navigate.
We start by creating a method to be used in the buttons to proceed.
Let's call it handlePressNextStep
which takes as a parameter the next step you want to go to (nextStep
).
Inside it, we create an object that will be responsible for having the step names as keys and the X-axis point that this component is present as values. Since pagingEnabled
is activated in the scrollView, as soon as it scrolls into the next step, the scrollView will center the scroll inside this step.
As the components have the width the same size as the device screen, we can say that the second step is present on the X-axis at the value of screen width - horizontal padding and step 3 is present at the value of step 2 * 2.
Steps:
1st - Welcome (Step 1) - starts at x: 0.
2nd - Account Information (Step 2) - starts at x: (screen width - horizontal padding).
3rd - Personal Information (Step 3) - starts at x: (screen width - horizontal padding) * 2.
4th - Complete (Step 4) - starts at x: (screen width - horizontal padding) * 3.
example: My device has a width of 390px, and I added 20px of horizontal padding, so the size of my steps will be 370px:
steps values: 390px - 20px = 370px
- So Welcome (Step 1) is at x-axis: 0.
- Account Information (Step 2) is at x-axis: 370px.
- Personal Information (Step 3) is at x-axis: 370px * 2 = 740px.
- Complete (Step 4) is at x-axis: 370px * 3 = 1110px.
With this information, we create the method below:
const handlePressNextStep = (nextStep: string) => {
const formSteps = {
account: stepFormWidth,
personal: stepFormWidth * 2,
complete: stepFormWidth * 3,
};
scrollRef?.current?.scrollTo({
x: formSteps[nextStep as keyof typeof formSteps],
});
};
Since we already started at the Welcome step, we can leave it out.
Navigate to the previous step
Just like in navigating to the next step, we create a method to scroll to the previous step, let's call it handlePressBackButton
and it takes as a parameter the previous step you want to go to (previousStep
).
const handlePressBackButton = (previousStep: string) => {
const formSteps = {
welcome: 0,
account: stepFormWidth,
};
scrollRef?.current?.scrollTo({
x: formSteps[previousStep as keyof typeof formSteps],
});
};
Using navigation
// src/app/index.tsx
import { useRef } from "react";
import { View, Text, Dimensions, Button, ScrollView } from "react-native";
import { normalizeFont } from "../utils/normalize-font";
const { width } = Dimensions.get("window");
export default function Home() {
const scrollRef = useRef<ScrollView>(null);
const screenPadding = 20;
const stepFormWidth = width - screenPadding * 2;
const handlePressNextStep = (nextStep: string) => {
const formSteps = {
account: stepFormWidth,
personal: stepFormWidth * 2,
complete: stepFormWidth * 3,
};
scrollRef?.current?.scrollTo({
x: formSteps[nextStep as keyof typeof formSteps],
});
};
const handlePressBackButton = (previousStep: string) => {
const formSteps = {
welcome: 0,
account: stepFormWidth,
};
scrollRef?.current?.scrollTo({
x: formSteps[previousStep as keyof typeof formSteps],
});
};
return (
// Screen Container
<View
style={{
flexGrow: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
}}
>
{/* Sign Up Container */}
<View
style={{
width: "100%",
borderRadius: 2,
backgroundColor: "2d0381",
}}
>
{/* Header */}
<View
style={{
backgroundColor: "#f0000099",
alignItems: "center",
justifyContent: "center",
padding: 40,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Header
</Text>
</View>
{/* Content Container */}
<ScrollView
style={{
flexGrow: 0,
alignContent: "center",
}}
contentContainerStyle={{
flexGrow: 1,
position: "relative",
}}
horizontal
pagingEnabled
ref={scrollRef}
>
<View
style={{
width: stepFormWidth,
backgroundColor: "gray",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Welcome (Step 1)
</Text>
<Button
title="continue"
color="#000"
onPress={() => {
handlePressNextStep("account");
}}
/>
</View>
<View
style={{
width: stepFormWidth,
backgroundColor: "green",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Button
title="previous step"
color="#000"
onPress={() => {
handlePressBackButton("welcome");
}}
/>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Account Information (Step 2)
</Text>
<Button
title="next step"
color="#000"
onPress={() => {
handlePressNextStep("personal");
}}
/>
</View>
<View
style={{
width: stepFormWidth,
backgroundColor: "orange",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Button
title="previous step"
color="#000"
onPress={() => {
handlePressBackButton("account");
}}
/>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Personal Information (Step 3)
</Text>
<Button
title="next step"
color="#000"
onPress={() => {
handlePressNextStep("complete");
}}
/>
</View>
<View
style={{
width: stepFormWidth,
backgroundColor: "red",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Complete (Step 4)
</Text>
</View>
</ScrollView>
</View>
</View>
);
}
Disabling scroll
Now that your scroll is working through button presses, it's necessary to disable scrolling so that the user can navigate solely through the buttons.
To do this, it's very simple. The ScrollView has the scrollEnabled
property, which is responsible for enabling or disabling its scroll. So, we set its value to false scrollEnabled={false}
, and thus scrolling will no longer be possible, only navigation through the buttons.
Building the Header
Now that we have the content container part with the steps and navigation between them via buttons, let's create the Header.
In the Header, we'll have some labels indicating which step the user is on and how many steps are left to complete the form.
// src/app/index.tsx
import { Dimensions, Text, View } from "react-native";
import { normalizeFont } from "../utils/normalize-font";
export default function Home() {
// ...
return (
// Screen Container
<View
style={{
flexGrow: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
}}
>
{/* Sign Up Container */}
<View
style={{
width: "100%",
borderRadius: 2,
backgroundColor: "#2d0381",
}}
>
{/* Header */}
<View
style={{
gap: 20,
padding: 20,
}}
>
<Text
style={{
fontSize: normalizeFont(20),
color: "#FAF9F6",
textTransform: "capitalize",
}}
>
Create account steps
</Text>
{/* Steps Indicator Container */}
<View
style={{
position: "relative",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
>
{/* StepIndicator Line */}
<View
style={{
position: "absolute",
height: 1,
width: "100%",
backgroundColor: "#FAF9F6",
}}
/>
{/* StepIndicator Label Wrapper - Welcome */}
<View
style={{
maxWidth: 100,
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
paddingVertical: 4,
paddingHorizontal: 8,
backgroundColor: "#9ca3af",
}}
>
<Text
style={{
fontSize: normalizeFont(12),
color: "#FAF9F6",
fontWeight: "bold",
}}
>
Welcome Step
</Text>
</View>
{/* StepIndicator Label Wrapper - Account Information */}
<View
style={{
maxWidth: 100,
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
paddingVertical: 4,
paddingHorizontal: 8,
backgroundColor: "#9ca3af",
}}
>
<Text
style={{
fontSize: normalizeFont(12),
color: "#FAF9F6",
fontWeight: "bold",
}}
>
Account Information
</Text>
</View>
{/* StepIndicator Label Wrapper - Personal Information */}
<View
style={{
maxWidth: 100,
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
paddingVertical: 4,
paddingHorizontal: 8,
backgroundColor: "#9ca3af",
}}
>
<Text
style={{
fontSize: normalizeFont(12),
color: "#FAF9F6",
fontWeight: "bold",
}}
>
Personal Information
</Text>
</View>
{/* Divisor */}
<View
style={{
position: "absolute",
bottom: -20,
height: 1,
width: "100%",
backgroundColor: "#9ca3af",
}}
/>
</View>
</View>
{/* Content Container */}
{/* ... */}
</View>
</View>
);
}
Logic to indicate if the step is completed
With the labels already present in the header, we can implement logic to change the style of the label if the step has been completed.
And how do we know if it has been completed?
In the content container, when navigating to the next step, we'll consider it completed, and a change in the style of the corresponding label will be reflected. If it returns to the previous step, it should become incomplete again.
To do this, let's create a state that will store the list of completed steps:
const [stepsCompleted, setStepsCompleted] = useState<string[]>([]);
And then, in the handlePressNextStep
and handlePressBackButton
functions, we implement the logic to add/remove the completed step from the list of steps.
The handlePressNextStep
function will receive one more parameter, which will be the currentStep
, and to better organize the parameters, let's put it as the first parameter of the function:
const handlePressNextStep = (currentStep: string, nextStep: string) => {
setStepsCompleted((prevState) =>
prevState.includes(currentStep) ? prevState : [...prevState, currentStep],
);
const formSteps = {
account: stepFormWidth,
personal: stepFormWidth * 2,
complete: stepFormWidth * 3,
};
scrollRef?.current?.scrollTo({
x: formSteps[nextStep as keyof typeof formSteps],
});
};
It's necessary to check before changing the state whether the current step is already in the list to avoid adding duplicate data to the list.
Now let's also add currentStep
as the first parameter in the handlePressBackButton
function:
const handlePressBackButton = (currentStep: string, previousStep: string) => {
setStepsCompleted((prevState) =>
// Validação adicionada para que retorne somente as etapas que sejam diferentes da atual e da anterior, deixando na lista apenas as etapas que já foram avançadas.
prevState.filter((step) => step !== currentStep && step !== previousStep),
);
const formSteps = {
welcome: 0,
account: stepFormWidth,
};
scrollRef?.current?.scrollTo({
x: formSteps[previousStep as keyof typeof formSteps],
});
};
The action buttons in the content container will look like this:
<ScrollView>
<View
//...
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Welcome (Step 1)
</Text>
<Button
title="continue"
color="#000"
onPress={() => {
handlePressNextStep("welcome", "account");
}}
/>
</View>
<View
//...
>
<Button
title="previous step"
color="#000"
onPress={() => {
handlePressBackButton("account", "welcome");
}}
/>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Account Information (Step 2)
</Text>
<Button
title="next step"
color="#000"
onPress={() => {
handlePressNextStep("account", "personal");
}}
/>
</View>
<View
//...
>
<Button
title="previous step"
color="#000"
onPress={() => {
handlePressBackButton("personal", "account");
}}
/>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Personal Information (Step 3)
</Text>
<Button
title="next step"
color="#000"
onPress={() => {
handlePressNextStep("personal", "complete");
}}
/>
</View>
<View
//...
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Complete (Step 4)
</Text>
</View>
</ScrollView>
Now when navigating to the next step, we add the current step to the list, and when returning a step, we remove it from the list, for example:
We are in the Account Information step, so the steps list will be ['welcome']
.
I moved to the Personal Information step, and the list will be ['welcome', 'account']
.
I returned to the Account Information step, and the list returned to ['welcome']
.
Changing the label style when completing the step
Now that we have a state with the list of completed steps, let's use it to determine if step "X" is present in the list. If it is, the background-color of the StepIndicator Label Wrapper
will be "#22c55e"
, otherwise, it will be "#9ca3af"
.
// src/app/index.tsx
import { useState } from "react";
//...
export default function Home() {
const [stepsCompleted, setStepsCompleted] = useState<string[]>([]);
// ...
return (
// Screen Container
<View
style={{
flexGrow: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
}}
>
{/* Sign Up Container */}
<View
style={{
width: "100%",
borderRadius: 2,
backgroundColor: "2d0381",
}}
>
{/* Header */}
<View
style={{
gap: 20,
padding: 20,
}}
>
<Text
style={{
fontSize: normalizeFont(20),
color: "#FAF9F6",
textTransform: "capitalize",
}}
>
Create account steps
</Text>
{/* Steps Indicator Container */}
<View
style={{
position: "relative",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
>
{/* StepIndicator Line */}
<View
style={{
position: "absolute",
height: 1,
width: "100%",
backgroundColor: "#FAF9F6",
}}
/>
{/* StepIndicator Label Wrapper - Welcome */}
<View
style={{
maxWidth: 100,
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
paddingVertical: 4,
paddingHorizontal: 8,
backgroundColor: stepsCompleted.includes("welcome")
? "#22c55e"
: "#9ca3af",
}}
>
<Text
style={{
fontSize: normalizeFont(12),
color: "#FAF9F6",
fontWeight: "bold",
}}
>
Welcome Step
</Text>
</View>
{/* StepIndicator Label Wrapper - Account Information */}
<View
style={{
maxWidth: 100,
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
paddingVertical: 4,
paddingHorizontal: 8,
backgroundColor: stepsCompleted.includes("account")
? "#22c55e"
: "#9ca3af",
}}
>
<Text
style={{
fontSize: normalizeFont(12),
color: "#FAF9F6",
fontWeight: "bold",
}}
>
Account Information
</Text>
</View>
{/* StepIndicator Label Wrapper - Personal Information */}
<View
style={{
maxWidth: 100,
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
paddingVertical: 4,
paddingHorizontal: 8,
backgroundColor: stepsCompleted.includes("personal")
? "#22c55e"
: "#9ca3af",
}}
>
<Text
style={{
fontSize: normalizeFont(12),
color: "#FAF9F6",
fontWeight: "bold",
}}
>
Personal Information
</Text>
</View>
{/* Divisor */}
<View
style={{
position: "absolute",
bottom: -20,
height: 1,
width: "100%",
backgroundColor: "#9ca3af",
}}
/>
</View>
</View>
{/* Content Container */}
{/* ... */}
</View>
</View>
);
}
Finished Code
And thus, we have the finished code with all the logic.
// src/app/index.tsx
import { useRef, useState } from "react";
import { Button, Dimensions, ScrollView, Text, View } from "react-native";
import { normalizeFont } from "../utils/normalize-font";
const { width } = Dimensions.get("window");
export default function Home() {
const [stepsCompleted, setStepsCompleted] = useState<string[]>([]);
const scrollRef = useRef<ScrollView>(null);
const screenPadding = 20;
const stepFormWidth = width - screenPadding * 2;
const handlePressNextStep = (currentStep: string, nextStep: string) => {
setStepsCompleted((prevState) =>
prevState.includes(currentStep) ? prevState : [...prevState, currentStep],
);
const formSteps = {
account: stepFormWidth,
personal: stepFormWidth * 2,
complete: stepFormWidth * 3,
};
scrollRef?.current?.scrollTo({
x: formSteps[nextStep as keyof typeof formSteps],
});
};
const handlePressBackButton = (currentStep: string, previousStep: string) => {
setStepsCompleted((prevState) =>
prevState.filter((step) => step !== currentStep && step !== previousStep),
);
const formSteps = {
welcome: 0,
account: stepFormWidth,
};
scrollRef?.current?.scrollTo({
x: formSteps[previousStep as keyof typeof formSteps],
});
};
return (
// Screen Container
<View
style={{
flexGrow: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
}}
>
{/* Sign Up Container */}
<View
style={{
width: "100%",
borderRadius: 2,
backgroundColor: "#2d0381",
}}
>
{/* Header */}
<View
style={{
gap: 20,
padding: 20,
}}
>
<Text
style={{
fontSize: normalizeFont(20),
color: "#FAF9F6",
textTransform: "capitalize",
}}
>
Create account steps
</Text>
{/* Steps Indicator Container */}
<View
style={{
position: "relative",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
>
{/* StepIndicator Line */}
<View
style={{
position: "absolute",
height: 1,
width: "100%",
backgroundColor: "#FAF9F6",
}}
/>
{/* StepIndicator Label Wrapper - Welcome */}
<View
style={{
maxWidth: 100,
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
paddingVertical: 4,
paddingHorizontal: 8,
backgroundColor: stepsCompleted.includes("welcome")
? "#22c55e"
: "#9ca3af",
}}
>
<Text
style={{
fontSize: normalizeFont(12),
color: "#FAF9F6",
fontWeight: "bold",
}}
>
Welcome Step
</Text>
</View>
{/* StepIndicator Label Wrapper - Account Information */}
<View
style={{
maxWidth: 100,
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
paddingVertical: 4,
paddingHorizontal: 8,
backgroundColor: stepsCompleted.includes("account")
? "#22c55e"
: "#9ca3af",
}}
>
<Text
style={{
fontSize: normalizeFont(12),
color: "#FAF9F6",
fontWeight: "bold",
}}
>
Account Information
</Text>
</View>
{/* StepIndicator Label Wrapper - Personal Information */}
<View
style={{
maxWidth: 100,
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
paddingVertical: 4,
paddingHorizontal: 8,
backgroundColor: stepsCompleted.includes("personal")
? "#22c55e"
: "#9ca3af",
}}
>
<Text
style={{
fontSize: normalizeFont(12),
color: "#FAF9F6",
fontWeight: "bold",
}}
>
Personal Information
</Text>
</View>
{/* Divisor */}
<View
style={{
position: "absolute",
bottom: -20,
height: 1,
width: "100%",
backgroundColor: "#9ca3af",
}}
/>
</View>
</View>
{/* Content Container */}
<ScrollView
style={{
flexGrow: 0,
alignContent: "center",
}}
contentContainerStyle={{
flexGrow: 1,
position: "relative",
}}
horizontal
pagingEnabled
scrollEnabled={false}
ref={scrollRef}
>
<View
style={{
width: stepFormWidth,
backgroundColor: "gray",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Welcome (Step 1)
</Text>
<Button
title="continue"
color="#000"
onPress={() => {
handlePressNextStep("welcome", "account");
}}
/>
</View>
<View
style={{
width: stepFormWidth,
backgroundColor: "green",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Button
title="previous step"
color="#000"
onPress={() => {
handlePressBackButton("account", "welcome");
}}
/>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Account Information (Step 2)
</Text>
<Button
title="next step"
color="#000"
onPress={() => {
handlePressNextStep("account", "personal");
}}
/>
</View>
<View
style={{
width: stepFormWidth,
backgroundColor: "orange",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Button
title="previous step"
color="#000"
onPress={() => {
handlePressBackButton("personal", "account");
}}
/>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Personal Information (Step 3)
</Text>
<Button
title="next step"
color="#000"
onPress={() => {
handlePressNextStep("personal", "complete");
}}
/>
</View>
<View
style={{
width: stepFormWidth,
backgroundColor: "red",
alignItems: "center",
justifyContent: "center",
paddingVertical: 160,
}}
>
<Text style={{ color: "white", fontSize: normalizeFont(30) }}>
Complete (Step 4)
</Text>
</View>
</ScrollView>
</View>
</View>
);
}
There are several improvements to implement in this little project, but since the focus of this article is on building a horizontal snap scroll with steps, we'll leave the improvements and refactorings for another article.
If you're interested, you can follow the project's improvements through the repository by clicking here.
Posted on February 21, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 21, 2024