One-Time Passwords (OTPs) have become a standard in two-factor authentication. Here's a ready-to-use, customizable OTP input component for your React Native application that includes validation, animations, and a resend timer.
Features
- Customizable length (default: 5 digits)
- Auto-focus to next input on entry
- Focus previous input on backspace
- Animation feedback on input focus
- Built-in countdown timer for OTP resend
- Fully typed with TypeScript
- Works with React Native Unistyles
Component Code
Note: You can use react native StyleSheet also instead of react-native-unistyles
.
import React, { useEffect, useRef, useState } from 'react';
import { View, TextInput, Animated, TouchableOpacity } from 'react-native';
import { createStyleSheet, useStyles } from 'react-native-unistyles';
import { boxShadow, hpx, wpx } from '@utils/Scaling';
import CustomText from './CustomText';
import { FONTS } from '@constants/Fonts';
interface OTPInputProps {
value: string[];
onChange: (value: string[]) => void;
length?: number;
disabled?: boolean;
onResendOTP?: () => void;
}
export const OTPInput: React.FC<OTPInputProps> = ({
value,
onChange,
length = 5,
disabled = false,
onResendOTP,
}) => {
const { styles, theme } = useStyles(stylesheet);
const inputRefs = useRef<TextInput[]>([]);
const animatedValues = useRef<Animated.Value[]>([]);
const [countdown, setCountdown] = useState(60);
const [isResendActive, setIsResendActive] = useState(false);
// Initialize animation values
useEffect(() => {
animatedValues.current = Array(length).fill(0).map(() => new Animated.Value(0));
}, [length]);
// Countdown timer
useEffect(() => {
let timer: NodeJS.Timeout;
if (countdown > 0 && !isResendActive) {
timer = setInterval(() => {
setCountdown((prev) => prev - 1);
}, 1000);
} else if (countdown === 0) {
setIsResendActive(true);
}
return () => {
if (timer) clearInterval(timer);
};
}, [countdown, isResendActive]);
const handleResendOTP = () => {
if (isResendActive && onResendOTP) {
onResendOTP();
setCountdown(60);
setIsResendActive(false);
// Focus on first input after a small delay to ensure state is updated
setTimeout(() => {
focusInput(0);
}, 50);
}
};
const focusInput = (index: number) => {
if (inputRefs.current[index]) {
inputRefs.current[index].focus();
// Trigger animation
Animated.sequence([
Animated.timing(animatedValues.current[index], {
toValue: 1,
duration: 100,
useNativeDriver: true,
}),
Animated.timing(animatedValues.current[index], {
toValue: 0,
duration: 100,
useNativeDriver: true,
}),
]).start();
}
};
const handleChange = (text: string, index: number) => {
const newValue = [...value];
newValue[index] = text;
onChange(newValue);
if (text && index < length - 1) {
focusInput(index + 1);
}
};
const handleKeyPress = (event: any, index: number) => {
if (event.nativeEvent.key === 'Backspace' && !value[index] && index > 0) {
focusInput(index - 1);
}
};
return (
<View style={styles.mainContainer}>
<View style={styles.container}>
{Array(length)
.fill(0)
.map((_, index) => {
const animatedStyle = {
transform: [
{
scale: animatedValues.current[index]?.interpolate({
inputRange: [0, 0.5, 1],
outputRange: [1, 1.1, 1],
}) || 1,
},
],
};
return (
<Animated.View key={index} style={[styles.inputContainer, animatedStyle]}>
<TextInput
ref={(ref) => {
if (ref) inputRefs.current[index] = ref;
}}
style={[
styles.input,
value[index] ? styles.filledInput : {},
]}
maxLength={1}
keyboardType="number-pad"
onChangeText={(text) => handleChange(text, index)}
onKeyPress={(event) => handleKeyPress(event, index)}
value={value[index]}
editable={!disabled}
selectTextOnFocus
placeholder="●"
placeholderTextColor={theme.colors.secondaryText}
/>
</Animated.View>
);
})}
</View>
<TouchableOpacity
onPress={handleResendOTP}
disabled={!isResendActive}
style={styles.resendContainer}
>
<CustomText
variant="sm"
style={{
color: theme.colors.navyBlueLine,
fontFamily: FONTS.SemiBold,
opacity: isResendActive ? 1 : 0.5
}}
>
{isResendActive ? 'Resend OTP' : `Resend OTP in ${countdown}s`}
</CustomText>
</TouchableOpacity>
</View>
);
};
const stylesheet = createStyleSheet(({ colors }) => ({
mainContainer: {
width: '100%',
},
container: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
marginVertical: hpx(20),
},
inputContainer: {
width: wpx(66),
height: hpx(80),
...boxShadow.light,
},
input: {
width: '100%',
height: '100%',
borderRadius: 10,
backgroundColor: colors.white,
textAlign: 'center',
fontSize: 24,
fontWeight: '600',
color: colors.typography,
},
filledInput: {
backgroundColor: colors.white,
borderColor: colors.primary,
},
resendContainer: {
alignItems: 'center',
},
}));
Usage Example
Here's how to implement the OTP component in your screen:
import React, { useState } from 'react';
import { View } from 'react-native';
import { OTPInput } from './components/OTPInput';
import CustomText from './components/CustomText';
const OTPScreen = ({ navigation }) => {
const [otpValues, setOtpValues] = useState(["", "", "", "", ""]);
const [otpError, setOtpError] = useState(null);
const handleOTPChange = (newValues) => {
setOtpValues(newValues);
setOtpError(null);
};
const handleResendOTP = () => {
// Reset OTP values
setOtpValues(["", "", "", "", ""]);
setOtpError(null);
// TODO: Add your API call to resend OTP here
};
const handleConfirm = () => {
const otp = otpValues.join('');
if (otp.length !== 5) {
setOtpError('Please enter a complete OTP');
return;
}
// Handle OTP verification here
navigation.navigate('Home'); // Replace with your navigation logic
};
return (
<View style={styles.container}>
<View style={styles.formContainer}>
<OTPInput
value={otpValues}
onChange={handleOTPChange}
length={5}
onResendOTP={handleResendOTP}
/>
{otpError && (
<CustomText style={styles.errorText} variant="sm">{otpError}</CustomText>
)}
</View>
{/* Add your button to submit OTP */}
<Button title="Confirm" onPress={handleConfirm} />
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
formContainer: {
marginBottom: 20,
},
errorText: {
color: 'red',
textAlign: 'center',
marginTop: 10,
},
});
export default OTPScreen;
Customization
You can customize:
- Number of OTP fields by changing the
length
prop - Countdown duration (default: 60s) by modifying the initial state
- Styling through the stylesheet
- Disable the component with the
disabled
prop
Dependencies
- React Native
- React Native Unistyles
- You'll need to create/modify:
-
CustomText
component -
boxShadow
,hpx
, andwpx
utility functions -
FONTS
constant
-
Conclusion
This component provides a complete solution for OTP input in React Native applications with proper focus management, animations, and a resend timer. It's designed to be easily integrated into your authentication flow with minimal setup.
Happy coding!
Top comments (0)