If you’ve used a mobile app in the past few years, odds are you’ve used your phone number to sign into an account. It’s a familiar process: enter your number, receive an SMS with a one-time code, and enter that code to sign in.
We can make this experience a little nicer for users by rendering a code input that is tailored to accept and display four to six digits.
Unfortunately, we can’t create this UI with a single TextInput
. Our strategy will be to capture the user’s input
with a hidden TextInput
and display the code with separate components.
Let’s start with the skeleton of our component. We’ll ignore the usual imports from React and React Native as well as the export of the component for the purposes of this post.
const CODE_LENGTH = 4;
const CodeInput = () => {
return <SafeAreaView style={style.container}></SafeAreaView>;
};
const style = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
Now we can add a hidden TextInput
to capture the code from the user as well as some state to
hold the code.
const CodeInput = () => {
const [code, setCode] = useState('');
return (
<SafeAreaView style={style.container}>
<TextInput
value={code}
onChangeText={setCode}
keyboardType="number-pad"
returnKeyType="done"
textContentType="oneTimeCode"
maxLength={CODE_LENGTH}
style={style.hiddenCodeInput}
/>
</SafeAreaView>
);
};
const style = StyleSheet.create({
...
hiddenCodeInput: {
position: 'absolute',
height: 0,
width: 0,
opacity: 0,
},
});
Next we need to display the code entered by the user and focus the hidden TextInput
when the code is pressed.
To display the code digits, we can map over an array of the code length. For each array element, we’ll key into the code
string with the index. If there is no character at the current index (if the code is
too short), we’ll render an empty space instead.
const CODE_LENGTH = 4;
const CodeInput = () => {
const codeDigitsArray = new Array(CODE_LENGTH).fill(0);
const toDigitInput = (_value: number, idx: number) => {
const emptyInputChar = ' ';
const digit = code[idx] || emptyInputChar;
return (
<View key={idx}>
<Text>{digit}</Text>
</View>
);
};
return (
<SafeAreaView style={style.container}>
<Pressable style={style.inputsContainer}>
{codeDigitsArray.map(toDigitInput)}
</Pressable>
<TextInput
value={code}
onChangeText={setCode}
keyboardType="number-pad"
returnKeyType="done"
textContentType="oneTimeCode"
maxLength={CODE_LENGTH}
style={style.hiddenCodeInput}
/>
</SafeAreaView>
);
};
...
Next, we want to focus the hidden TextInput
when the user presses the code. To
imperatively focus the TextInput
, we will need to use a ref.
const ref = useRef<TextInput>(null);
const handleOnPress = () => {
ref?.current?.focus();
};
...
<SafeAreaView style={style.container}>
<Pressable style={style.inputsContainer} onPress={handleOnPress}>
{codeDigitsArray.map(toDigitInput)}
</Pressable>
<TextInput
ref={ref}
value={code}
onChangeText={setCode}
keyboardType="number-pad"
returnKeyType="done"
textContentType="oneTimeCode"
maxLength={CODE_LENGTH}
style={style.hiddenCodeInput}
/>
</SafeAreaView>
...
Now our code input is fully functional!
Next, we’ll style the code input as a whole and the individual digits. Then we can add some conditional styling to highlight the current digit.
const toDigitInput = (_value: number, idx: number) => {
const emptyInputChar = ' ';
const digit = code[idx] || emptyInputChar;
return (
<View key={idx} style={style.inputsContainer}>
<Text style={style.inputText}>{digit}</Text>
</View>
);
};
...
<SafeAreaView style={style.container}>
<Pressable style={style.inputsContainer} onPress={handleOnPress}>
{codeDigitsArray.map(toDigitInput)}
</Pressable>
<TextInput
ref={ref}
value={code}
onChangeText={setCode}
keyboardType="number-pad"
returnKeyType="done"
textContentType="oneTimeCode"
maxLength={CODE_LENGTH}
style={style.hiddenCodeInput}
/>
</SafeAreaView>
...
const style = StyleSheet.create({
...
inputsContainer: {
width: '60%',
flexDirection: 'row',
justifyContent: 'space-between',
},
inputContainer: {
borderColor: '#cccccc',
borderWidth: 2,
borderRadius: 4,
padding: 12,
},
inputText: {
fontSize: 24,
fontFamily: 'Menlo-Regular',
},
...
});
Time for some conditional styling.
First, we’ll introduce some state to hold the focus state, which we will toggle when the user focuses and blurs the input. Finally, we’ll apply some styling to highlight the current digit.
The following must be true for the current digit to be highlighted:
- the code input as a whole is focused by the user
containerIsFocused === true
AND
- the index of the digit is equal to the current length of the inputted code
const isCurrentDigit = idx === code.length;
OR
- the code is full and it’s the last digit
const isLastDigit = idx === CODE_LENGTH - 1;
const isCodeFull = code.length === CODE_LENGTH;
Applied to the component, the final styling logic looks like this:
const CODE_LENGTH = 4;
const CodeInput = () => {
...
const [containerIsFocused, setContainerIsFocused] = useState(false);
const handleOnPress = () => {
setContainerIsFocused(true);
ref?.current?.focus();
};
const handleOnBlur = () => {
setContainerIsFocused(false);
};
...
const toDigitInput = (_value: number, idx: number) => {
const emptyInputChar = ' ';
const digit = code[idx] || emptyInputChar;
const isCurrentDigit = idx === code.length;
const isLastDigit = idx === CODE_LENGTH - 1;
const isCodeFull = code.length === CODE_LENGTH;
const isFocused = isCurrentDigit || (isLastDigit && isCodeFull);
const containerStyle =
containerIsFocused && isFocused
? {...style.inputContainer, ...style.inputContainerFocused}
: style.inputContainer;
return (
<View key={idx} style={containerStyle}>
<Text style={style.inputText}>{digit}</Text>
</View>
);
};
return (
...
<TextInput
ref={ref}
value={code}
onChangeText={setCode}
onSubmitEditing={handleOnBlur}
keyboardType="number-pad"
returnKeyType="done"
textContentType="oneTimeCode"
maxLength={CODE_LENGTH}
style={style.hiddenCodeInput}
/>
...
)
...
};
That’s it! Here’s the GitHub repo for the code.
Speed up development
Get your app to market faster with thoughtbot’s proven and reliable approach to mobile design and development. Learn more about how thoughtbot can deliver better and faster outcomes for your mobile app.