DEV Community

Cover image for Building a React Native number pad feature from scratch
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Building a React Native number pad feature from scratch

Written by Taofiq Aiyelabegan✏️

Modern mobile apps often involve a step in the onboarding process where you need to enter a verification PIN sent to your email or phone number. Sometimes, you’ll need to enter the PIN using something like a split OTP input field. Another way to input a verification PIN is by using a dialpad.

In this article, we will demonstrate how to create a custom number pad designed for React Native apps. Building a custom React Native numpad can be a great alternative to split inputs or the conventional TextInput element for personalizing your mobile application design.

You can check out the full source code for our React Native project and follow along as we set up the number pad step by step. Let’s get started.

Use cases for a number pad in React Native apps

There are many professional use cases for a numpad in React Native apps.

A common example is one-time passcode (OTP) input validation. For example, let’s say you send an OTP to a new app user’s mobile number as part of the onboarding process. After sending the OTP, the user is directed to a screen to input and validate it using a number pad.

Another use case is adding a layer of security to your app, which is important for apps containing sensitive information. When your users log back into your application, you can present them with a number pad where they can input a PIN for your app to validate before logging them in.

In our tutorial, we’ll create a simple example of this second use case. We’ll see how to set up a number pad from scratch in React Native so users can create a PIN and log into the app using that PIN.

Setting up the development environment

Let’s get started with building a React Native app with a custom number pad feature. We will be using the Expo CLI for development, but you can also use the React Native CLI if you wish.

Run the following command to quickstart an Expo app:

npx create-expo-app my-app
Enter fullscreen mode Exit fullscreen mode

The command above will create the basic React Native project files we need. Once that’s done, start up the development server on the iOS or Android simulator:

//for iOS
npm run ios 
//for Android
npm run android
Enter fullscreen mode Exit fullscreen mode

After running the appropriate command, you should have a screen like this pop up in your simulator: Basic React Native App Set Up In Expo Displaying Prompt To Edit App Js File To Build App This is the output of the code inside the App.js file in your project folder.

Creating, rendering, and styling the React Native number pad

In this section, we will begin creating three screens: Login, CustomDialpad, and Home.

The Login screen will be the first screen users see when they initially load the app. It will have a button that moves the user to the CustomDialpad screen, where they can input their PIN. Once the correct PIN is entered, the app will move the user to the Home screen.

Let’s start building our React Native app with these three screens. First, install the following packages that we need to set up and configure basic navigation in React Native:

npx install @react-navigation/native @react-navigation/native-stack react-native-safe-area-context react-native-screens
Enter fullscreen mode Exit fullscreen mode

Also, create a folder named screens with three files inside it: Login.jsx, CustomDialPad.jsx, and HomeScreen.jsx.

Next, inside your App.js file, implement the basic navigation as shown below:

import { StyleSheet } from "react-native";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import HomeScreen from "./screens/HomeScreen";
import Login from "./screens/Login";
import CustomDialPad from "./screens/CustomDialPad";

const Stack = createNativeStackNavigator();
export default function App() {
 return (
   <NavigationContainer>
     <Stack.Navigator
       screenOptions={{
         headerShown: false,
       }}
     >
       <Stack.Screen name="Login" component={Login} />
       <Stack.Screen name="Dialpad" component={CustomDialPad} />
       <Stack.Screen name="Home" component={HomeScreen} />
     </Stack.Navigator>
   </NavigationContainer>
 );
}

const styles = StyleSheet.create({
 container: {
   flex: 1,
   backgroundColor: "#fff",
   alignItems: "center",
   justifyContent: "center",
 },
});
Enter fullscreen mode Exit fullscreen mode

Here, we wrap the entire app inside the NavigationContainer and use the Stack.Navigator component to manage the stack of screens. When a user navigates to a screen, it gets pushed to the top of the stack. Then, when the user navigates to another page, it pops the screen off the top of the stack.

In this case, the initial screen at the top of the stack will be the Login screen. When the user presses the button to navigate to the CustomDialpad screen, the CustomDialpad screen is pushed on top of the Login screen, and so on: Demo Of Screen Navigation Set Up With User Moving Between Three Screens: Login, Number Pad, And Home Screen Now that the screen navigation is all set up, we can begin setting up the number pad logic and UI.

Setting up the CustomDialpad.jsx file

Create a component folder in the root directory and add a DialpadKeypad.jsx file inside it. We’ll build our number pad UI and functionality in this file later.

Then, import the DialpadKeypad.jsx component file inside the CustomDialpad.jsx file:

//CustomDialPad.js

import {
 SafeAreaView,
 StyleSheet,
 Text,
 View,
 Dimensions,
} from "react-native";
import React, { useState } from "react";
import { Ionicons } from "@expo/vector-icons";
import DialpadKeypad from "../components/DialpadKeypad";

const { width, height } = Dimensions.get("window");
Enter fullscreen mode Exit fullscreen mode

We also used the Dimensions.get method to extract the screen width and height of the user’s device. This will help us ensure our UI adapts responsively to different screen sizes.

Next, to start building our React Native number pad, we first need to create some variables:

const dialPadContent = [1, 2, 3, 4, 5, 6, 7, 8, 9, "", 0, "X"];

const dialPadSize = width * 0.2;
const dialPadTextSize = dialPadSize * 0.4;

const pinLength = 4;
Enter fullscreen mode Exit fullscreen mode

Let’s briefly review the purpose of each variable:

  • dialPadContent — The content that we’ll display on the number pad UI. In this case, we want to display an array of twelve values arranged in a three-column, four-row grid
  • pinLength — The length of the PIN the user should enter. We want the user to enter a four-digit PIN, but this can be adjusted based on your project’s needs
  • dialPadSize — The size of the number pad, which is determined by multiplying the mobile phone screen’s width by 0.2 to get 20 percent of the screen’s width
  • dialPadTextSize — The size of the text displayed inside the number pad, which is determined by multiplying the dialPadSize value by 0.4 to get 40 percent of the dialPadSize

In the rest of the CustomDialpad.jsx file, we define our CustomDialPad component and use the useNavigation Hook to enable us to control screen navigation. We also set up the component structure and styles, and export the custom component to make it available for use in other parts of our app:

const CustomDialPad = () => {
 const navigation = useNavigation();
 return (
   <SafeAreaView style={styles.container}>
     <View style={styles.textContainer}>
       <TouchableOpacity
         onPress={() => navigation.goBack()}
         style={{ position: "absolute", top: -5, left: 10 }}
       >
         <Ionicons name="ios-chevron-back" size={45} color="#5E454B" />
       </TouchableOpacity>
       <Text style={styles.pinText}>Create PIN</Text>
       <Text style={styles.pinSubText}>Enter your secure four-digit code</Text>
       <DialpadKeypad
         dialPadContent={dialPadContent}
         pinLength={pinLength}
         dialPadSize={dialPadSize}
         dialPadTextSize={dialPadTextSize}
       />
     </View>
   </SafeAreaView>
 );
};

export default CustomDialPad;

const styles = StyleSheet.create({
 container: {
   flex: 1,
   backgroundColor: "#FAF0E6",
 },
 textContainer: {
   justifyContent: "center",
   alignItems: "center",
   marginTop: 40,
   position: "relative",
 },
 pinText: {
   fontSize: 30,
   fontWeight: "medium",
   color: "#5E454B",
 },
 pinSubText: {
   fontSize: 18,
   fontWeight: "medium",
   color: "#5E454B",
   marginVertical: 30,
 },
Enter fullscreen mode Exit fullscreen mode

Additionally, we implemented a back button with the TouchableOpacity component, enabling the user to navigate back to the Login page using the navigation.goBack() method.

Setting up the DialpadKeypad.jsx file

Now let’s work in the DialpadKeypad.js file. First, we’ll import all the necessary modules and components:

import {
 StyleSheet,
 Text,
 View,
 FlatList,
 TouchableOpacity,
} from "react-native";
import React from "react";
import { Feather } from "@expo/vector-icons";
Enter fullscreen mode Exit fullscreen mode

Next, let’s take the props that we passed to the component in the CustomDialPad.js file and use them to build the keypad UI. Then, we’ll use a Flatlist to render the dialPadContent array we defined earlier:

const DialpadKeypad = ({
 dialPadContent,
 pinLength,
 navigation,
 dialPadSize,
 dialPadTextSize,
}) => {
 return (
   <FlatList
     data={dialPadContent}
     numColumns={3} // set number of columns
     keyExtractor={(_, index) => index.toString()}
     renderItem={({ item }) => {
       return (
         <TouchableOpacity
           disabled={item === ""} // make the empty space on the dialpad content unclickable
          >
           <View
             style={[
               {
                 backgroundColor: item === "" ? "transparent" : "#fff",
                 width: dialPadSize,
                 height: dialPadSize,
               },
               styles.dialPadContainer,
             ]}
           >
             {item === "X" ? (
               <Feather name="delete" size={24} color="#3F1D38" />
             ) : (
               <Text
                 style={[{ fontSize: dialPadTextSize }, styles.dialPadText]}
               >
                 {item}
               </Text>
             )}
           </View>
         </TouchableOpacity>
       );
     }}
   />
 );
};
Enter fullscreen mode Exit fullscreen mode

We set the numColumns prop to 3 to render our dialPadContent array in three columns. The blank "" value in the array lets us make the rendered three-column, four-row number pad more visually balanced.

In the number pad, we made the blank button unpressable and removed its background color. We also rendered a delete icon for the button corresponding to the X value in the array. Otherwise, we render the numbers in the array for the rest of the buttons in the number pad.

We also wrapped the View component inside the TouchableOpacity component to render the dialpadContent.

Our last step in this file is to define our component's styles:

export default DialpadKeypad;

const styles = StyleSheet.create({
 dialPadContainer: {
   justifyContent: "center",
   alignItems: "center",
   margin: 10,
   borderRadius: 50,
   borderColor: "transparent",
 },
 dialPadText: {
   color: "#3F1D38",
 },
});
Enter fullscreen mode Exit fullscreen mode

Let’s look at the React Native number pad we have currently: React Native Number Pad User Interface Set Up Displaying Screen Titled Create Pin With Prompt To Enter Secure Four Digit Code And Number Pad Below Including Blank Button And Back Button

Integrating and limiting the press functionality

Let’s set up the functionality for when we press a button on the keypad. We’ll track the value of each button pressed on the keypad using a state where the initial data type will be an array. This will then be passed as a prop to the DialpadKeypad component.

In the DialpadKeypad file, we’ll take the code and setCode props and use them to implement the desired functionality. When the Keypad content is clicked, we’ll call the onPress prop to first check:

  • If the button pressed has a value of X. If so, it should remove the last item in the array — in other words, delete the last PIN value selected
  • If the value of the pressed button has any value other than X. If so, it should use the setCode prop to add the selected item into the code array
  • If the code array length is equal to the pinLength - 1. If so, it should navigate the user to the Home screen

The reason we’re using pinLength - 1 against the length of the code prop is because the required pinLength is specified as 4.

If there are four items in the code state array, the length will be 3, since indexed values in an array start from 0. Therefore, to navigate to the Home screen once the four-digit PIN has been entered into the code array, we use pinLength -1.

To implement all of this, we need to update the code in the CustomDialPad.js file like so:

const CustomDialPad = () => {
 const navigation = useNavigation();
 const [code, setCode] = useState([]);

// rest of the code 

  <DialpadKeypad
         dialPadContent={dialPadContent}
         pinLength={pinLength}
         setCode={setCode}
         code={code}
         dialPadSize={dialPadSize}
         dialPadTextSize={dialPadTextSize}
       />
Enter fullscreen mode Exit fullscreen mode

Likewise, update the DialpadKeypad.js file like so:

const DialpadKeypad = ({
 dialPadContent,
 pinLength,
 code,
 setCode,
 navigation,
 dialPadSize,
 dialPadTextSize,
}) => {

// rest of the code
    <TouchableOpacity
           disabled={item === ""} // make the empty space on the dialpad content unclickable
           onPress={() => {
             if (item === "X") {
               setCode((prev) => prev.slice(0, -1));
             } else {
               if (code.length === pinLength - 1) {
                 navigation.navigate("Home");
               }
               setCode((prev) => [...prev, item]);
             }
           }}
         >
Enter fullscreen mode Exit fullscreen mode

Adding a MultiView for the entered PIN

In this section, we will add a MultiView. In this instance, this is a View that will allow us to view the selected input — in other words, the entered PIN.

To begin, create a DialpadPin.js file inside the component folder and render it in the CustomDialPad component. Then we will pass the pinLength, pinSize, code, and dialPadContent props to the DialpadPin.js file.

Inside the DialpadPin.js file, we will render a View based on the PIN length of 4 that we established earlier. We want to display this on the CustomDialpad screen as a row of four evenly spaced out circles between the prompt to enter the PIN and the number pad.

Inside the rendered View, we will also render the PIN value, which will let us know whether a value has been selected. If a value has been selected from the keypad, we will display it in the MultiView so users know how many digits they currently have selected in the input.

To implement all of this, update the CustomDialPad.js file like so:

const dialPadContent = [1, 2, 3, 4, 5, 6, 7, 8, 9, "", 0, "X"];

const dialPadSize = width * 0.2;
const dialPadTextSize = dialPadSize * 0.4;

const pinLength = 4;
const pinContainerSize = width / 2;
const pinSize = pinContainerSize / pinLength;
const CustomDialPad = () => {
 const fontsLoaded = useCustomFonts();
 const navigation = useNavigation();
 const [code, setCode] = useState([])

// rest of the code 

       <DialpadPin
         pinLength={pinLength}
         pinSize={pinSize}
         code={code}
         dialPadContent={dialPadContent}
       />
Enter fullscreen mode Exit fullscreen mode

Then, update the DialpadPin.js file as well:

import { StyleSheet, Text, View } from "react-native";
import React from "react";

const DialpadPin = ({ pinLength, pinSize, code, dialPadContent }) => {
 return (
   <View style={styles.dialPadPinContainer}>
     {Array(pinLength)
       .fill()
       .map((_, index) => {
         const item = dialPadContent[index];
         const isSelected =
           typeof item === "number" && code[index] !== undefined;
         return (
           <View
             key={index}
             style={{
               width: pinSize,
               height: pinSize,
               borderRadius: pinSize / 2,
               overflow: "hidden",
               margin: 5,
             }}
           >
             <View
               style={[
                 {
                   borderRadius: pinSize / 2,
                   borderColor: !isSelected ? "lightgrey" : "#3F1D38",
                 },
                 styles.pinContentContainer,
               ]}
             >
               {isSelected && (
                 <View
                   style={[
                     {
                       width: pinSize * 0.5,
                       height: pinSize * 0.5,
                       borderRadius: pinSize * 0.35,
                     },
                     styles.pinContent,
                   ]}
                 />
               )}
             </View>
           </View>
         );
       })}
   </View>
 );
};

export default DialpadPin;

const styles = StyleSheet.create({
 dialPadPinContainer: {
   flexDirection: "row",
   marginBottom: 30,
   alignItems: "flex-end",
 },
 pinContentContainer: {
   flex: 1,
   backgroundColor: "#fff",
   borderWidth: 1,
   justifyContent: "center",
   alignItems: "center",
 },
 pinContent: {
   backgroundColor: "#5E454B",
 },
});
Enter fullscreen mode Exit fullscreen mode

Now, let’s see what we have: Demo Of React Native Number Pad Feature In Action With User Shown Inputting Pin To Fill Multiview Component With Colored Dots As you can see, inputting or removing digits using the number pad fills in the MultiView accordingly. To protect the user’s security, the selected PIN values are shown as colored dots instead of displaying the actual numbers.

We can go a step further to animate the dotted pin selected from the number pad. In the DialpadPin.jsx file, import the Animated library, which is provided out of the box by React Native. Then, wrap the View that shows the dotted selection with Animated.View:

 {isSelected && (
    <Animated.View
     style={[
         {
           width: pinSize * 0.5,
           height: pinSize * 0.5,
           borderRadius: pinSize * 0.35,
          },
     styles.pinContent,
    ]}
   />
  )}
Enter fullscreen mode Exit fullscreen mode

Now we will create a useEffect Hook that will be triggered whenever the value of the code changes. Anytime the user enters a number on the keypad, the animation will be triggered using the Animation.timing method. The animatedValue is animated from its current value to the code.length value over 300 milliseconds:

const DialpadPin = ({ pinLength, pinSize, code, dialPadContent }) => {
 const animatedValue = useRef(new Animated.Value(0)).current;

 useEffect(() => {
   Animated.timing(animatedValue, {
     toValue: code.length,
     duration: 300,
     useNativeDriver: true,
   }).start();
 }, [code]);
Enter fullscreen mode Exit fullscreen mode

Next, we’ll use the animatedStyle style object to apply scaling transformations when a number is selected on the keypad:

 const animatedStyle = {
           transform: [
             {
               scale: animatedValue.interpolate({
                 inputRange: [index, index + 1],
                 outputRange: [1, 1.3],
                 extrapolate: "clamp",
               }),
             },
           ],
         };
Enter fullscreen mode Exit fullscreen mode

We used the interpolate method here to map the input value to the output value, ensuring smooth animations. The inputRange and outputRange props define the value for the interpolation.

Lastly, the extrapolate prop defines the behavior of the output value. Its clamp value indicates that the output value is clamped within the defined range. Add the animatedStyle object to the styling input for the Animated.View:

   {isSelected && (
                 <Animated.View
                   style={[
                     {
                       width: pinSize * 0.5,
                       height: pinSize * 0.5,
                       borderRadius: pinSize * 0.35,
                     },
                     styles.pinContent,
                     animatedStyle,
                   ]}
                 />
               )}
Enter fullscreen mode Exit fullscreen mode

The final result with our added animation should look like the below: Demo Of React Native Number Pad Feature In Action With Added Animation Effect For Colored Dots Filling In Multiview Bubbles As you can see, the colored dots appear in the MultiView bubbles slightly smaller at first, then expands to fill the bubble more completely. This makes our number pad feature more visually engaging in a subtle way that isn’t overly distracting.

Additional notes and recommendations

To improve this number pad implementation in a real React Native app meant for use in production, we would need to set up a backend service to talk to our frontend implementation. Let’s review what this would entail for each of our use cases.

The first use case we discussed uses the number pad to verify an OTP sent to the user’s phone or email during onboarding. So, when a new user signs up for your app, you would need to:

  • Verify the email they used to sign up
  • Send the OTP from your backend service
  • Direct them to a screen containing a number pad where they can enter the OTP you sent to their email

Now the user is expected to use the number pad to enter the OTP they received. Ideally, when they enter the complete OTP, you should be able to send a request to a verify endpoint in your backend to verify that the OTP you sent to that user matches what they entered on the frontend:

  • If it matches, navigate them to the Home screen
  • If it doesn’t match, display a customized error message informing them that the PIN entered is wrong and they should enter the right PIN sent to their email

In our current project, we aren’t verifying the PIN because we don’t have a backend service set up. However, if you were to set this up in a real project, the verify endpoint should be called in the DialpadKeypad.js file, where we’re checking the code.length against the pinLength:

<TouchableOpacity
  disabled={item === ""} // make the empty space on the dialpad content unclickable
  onPress={() => {
  if (item === "X") {
   setCode((prev) => prev.slice(0, -1));
   } else {
    if (code.length === pinLength - 1) {
// call the endpoint here, once the user enters the required pin length
     navigation.navigate("Home");
    }
    setCode((prev) => [...prev, item]);
    }
    }}
   >
Enter fullscreen mode Exit fullscreen mode

The second use case we discussed is using the number pad for login security. Just as in the first use case, you can customize the number pad in your app to show on your login page.

The user can enter a PIN when they sign up. Then, when the user re-enters their PIN to sign back into the app, you can have your backend endpoint verify that the password created during signup matches the one being entered.

If your backend endpoint verifies the match, you can then allow the user to log in. If not, you can show a customized alert message — for example, Pin does not match.

This use case ensures that users are not just entering the app without the necessary security checks in place.

Comparing methods for creating a custom number pad

React Native supports a few different methods for creating a number pad. For example, we could use the TextInput component and pass the keyboard type as numeric to set up our number pad. However, there are some known issues associated with this method:

  • Failure to dismiss when tapping outside the component: This issue means the number pad remains open even when you tap outside of the TextInput. A possible workaround for this issue is using the TouchableWithoutFeedback API component to dismiss the TextInput keyboard when you tap outside of it
  • Failure to dismiss on press of the return key: This issue means the number pad does not auto-dismiss when you press the return key

There are also a few existing open source libraries for a number pad feature, including React Native Numpad and React Native Numeric Pad. However, these libraries are somewhat limited in their features and customizability.

In many cases, your React Native application may have a unique design and particular requirements regarding how the number pad feature should be built and implemented. Building a custom feature means you won’t be limited by the library’s capabilities.

In addition, installing too many packages in your React Native application can make it heavy. Building your own features and installing fewer packages can help reduce the size of your application.

Lastly, libraries may not remain actively maintained and could even become abandoned entirely, which could crash your application. If you do opt for a third-party library, always try to use stable and well-maintained options.

Here’s a quick summary of your available options for creating number pads in React Native:

Method Pros Cons
`TextInput` component - Built-in React Native component, which helps achieve platform consistency out of the box - Quick setup and ease of use as you only need to import the API component in your project and use it straight up - Might not be the best choice to use if you want to customize the design for your app. - Known performance and optimization issues
[React Native Numpad library](https://github.com/glancemoney/react-native-numpad) - Simplified numeric input feature provided out of the box - Ability to enter both decimals and integers - Cross-platform compatinility - Limited customization options - Complex learning curve
[React Native Numeric Pad library](https://github.com/RidicZhi/react-native-numeric-pad) - Provides a dedicated numeric keypad input out of the box - Ability to customize its appearance and behavior - Limited customization options - Complex learning curve
Custom number pad - Ability to customize the number pad to suit your overall app design - No feature limitations; you can implement functionalities that might not be available out of the box using the conventional numeric keypad (e.g input validation, actions on specific buttons, animations) - Takes time to develop - Achieving consistent behavior and functionality across iOS and Android platforms could be a challenge during development

The method you choose boils down to your project’s needs. For example, using libraries can help you save significant development time. However, if you need particular features or customizations, it may be better to invest time in building your own.

Conclusion

In this article, we have learned how to create a custom number pad in React Native. We also compared our method with other options, like the built-in TextInput component and third-party open source libraries, to better understand when and why to build this feature from scratch.

A custom number pad is a great mobile app feature for use cases like authenticating users with one-time passcodes or having them log in using a PIN. You can find the full source code for our demo project in this repository.


LogRocket: Instantly recreate issues in your React Native apps

LogRocket Signup

LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.

LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.

Start proactively monitoring your React Native apps — try LogRocket for free.

Top comments (0)