Introduction
One-time password (OTP) verification has become essential in mobile applications for secure user authentication. While several libraries offer OTP components but i think we can create easily with our custom components. This article guides you through creating a custom, animated OTP Input component in React Native that offers a sleek, user-friendly interface.
Features of Our OTP Component
- Smooth Cursor Blink Animation
- Shake Animation for Invalid OTP
- Auto-Focus & Smart Backspace Navigation
- Countdown Timer with Resend OTP Feature
- Native-Looking Input Boxes Without External UI Libraries
- Easy Integration with Any Parent Screen
Component Goals
Our OTP component aims to be:
- Modern in Appearance: Rounded input boxes with smooth transitions.
- Intelligent in Behavior: Auto-focus on the next input and manageable state flows.
- Engaging with Micro-Animations: Cursor blinking, box-scale on focus, and shake effects for error notifications.
- Reusable: Easy to incorporate with simple prop configurations.
- Cross-Compatible: Works on both iOS and Android, supporting light and dark themes.
Building the OTP Input Component
Component Structure
The OTP component utilizes:
-
TextInputfor each digit box -
Animated.Valuefor various animations - Hooks like
useEffect,useRef, anduseImperativeHandle
Core Functionalities
1. Cursor Blink Animation
The cursor is simulated with a small vertical bar using opacity animation. This enhances the UX by only showing the cursor for the active input.
Animated.loop(
Animated.sequence([
Animated.timing(cursorOpacity, { toValue: 0, duration: 450 }),
Animated.timing(cursorOpacity, { toValue: 1, duration: 450 }),
])
).start();
2. Shake Animation on Errors
To indicate an invalid OTP, trigger a shake animation:
otpRef.current?.triggerShake();
The shake uses an interpolated translateX to create an effective feedback mechanism.
translateX: shakeAnim.interpolate({ inputRange: [-1, 1], outputRange: [-10, 10] })
3. Countdown Timer with Resend OTP
A built-in timer starts at 60 seconds, allowing for a seamless resend experience:
useEffect(() => {
if (countdown > 0) {
setInterval(() => setCountdown(prev => prev - 1), 1000);
}
});
4. Auto-Focus & Smart Navigation
The input fields are designed for intuitive navigation. Typing in one box automatically shifts focus to the next, while backspacing on an empty box moves the focus to the previous input.
5. Box Scale Animation
Every focused input box features a subtle scaling effect, enhancing perceived interactivity:
transform: [{ scale: animatedValues.current[index].interpolate({...}) }]
Integrating the OTP Component
Here’s how to integrate the OTPInputComponent into your parent screen:
const [otp, setOtp] = useState(['', '', '', '']); // if we use lenght 4 then 4 empyt string
const submitOtp = () => {
console.log('OTP entered:', otp);
};
<OTPInputComponent
length={4} // it is dynamic length
value={otp}
onChange={(code) => setOtp(code)}
/>
<Button title="Submit" onPress={submitOtp} />
Complete OTP Component Code
Here is the comprehensive code for the OTPInputComponent component:
import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';
import { View, Text, TextInput, TouchableOpacity, Animated, StyleSheet, Dimensions } from 'react-native';
const { width } = Dimensions.get('window');
interface InputProps {
value: string[];
onChange: (value: string[]) => void;
length?: number;
disabled?: boolean;
onResendOTP?: () => void;
}
export const OTPInputComponent = forwardRef(({ value, onChange, length = 5, disabled = false, onResendOTP }: InputProps, ref) => {
const inputRefs = useRef<TextInput[]>([]);
const animatedValues = useRef<Animated.Value[]>([]);
const [countdown, setCountdown] = useState(60);
const [isResendActive, setIsResendActive] = useState(false);
const cursorOpacity = useRef(new Animated.Value(1)).current;
const shakeAnim = useRef(new Animated.Value(0)).current;
useImperativeHandle(ref, () => ({ triggerShake }));
useEffect(() => {
Animated.loop(Animated.sequence([
Animated.timing(cursorOpacity, { toValue: 0, duration: 450 }),
Animated.timing(cursorOpacity, { toValue: 1, duration: 450 }),
])).start();
}, [cursorOpacity]);
useEffect(() => {
animatedValues.current = Array(length).fill(0).map(() => new Animated.Value(0));
}, [length]);
useEffect(() => {
const timer = countdown > 0 && !isResendActive ? setInterval(() => setCountdown(prev => prev - 1), 1000) : null;
if (countdown === 0) setIsResendActive(true);
return () => timer && clearInterval(timer);
}, [countdown, isResendActive]);
const triggerShake = () => {
shakeAnim.setValue(0);
Animated.sequence([
Animated.timing(shakeAnim, { toValue: 1, duration: 80, useNativeDriver: true }),
Animated.timing(shakeAnim, { toValue: -1, duration: 80, useNativeDriver: true }),
Animated.timing(shakeAnim, { toValue: 1, duration: 80, useNativeDriver: true }),
Animated.timing(shakeAnim, { toValue: 0, duration: 80, useNativeDriver: true }),
]).start();
};
const handleChange = (text: string, index: number) => {
const newValue = [...value];
newValue[index] = text;
onChange(newValue);
if (text && index < length - 1) inputRefs.current[index + 1].focus();
};
const handleResendOTP = () => {
if (isResendActive && onResendOTP) {
onResendOTP();
setCountdown(60);
setIsResendActive(false);
}
};
return (
<View style={styles.mainContainer}>
<Animated.View style={[styles.container, { transform: [{ translateX: shakeAnim.interpolate({ inputRange: [-1, 1], outputRange: [-10, 10] }) }] }]}>
{Array(length).fill(0).map((_, index) => (
<View key={index} style={styles.inputContainer}>
<TextInput
ref={ref => (inputRefs.current[index] = ref)}
style={styles.input}
maxLength={1}
keyboardType="number-pad"
onChangeText={text => handleChange(text, index)}
value={value[index]}
editable={!disabled}
/>
{index === 0 && <Animated.View style={[styles.cursor, { opacity: cursorOpacity }]} />}
</View>
))}
</Animated.View>
<TouchableOpacity onPress={handleResendOTP} disabled={!isResendActive}>
<Text style={styles.resendText}>{isResendActive ? 'Resend OTP' : `Resend OTP in ${countdown}s`}</Text>
</TouchableOpacity>
</View>
);
});
const styles = StyleSheet.create({
// Define your styles here...
});
Conclusion
This OTP input component exemplifies the fusion of functionality, reusability, and modern design practices.
Feel free to use the code and adapt it for your application needs!

Top comments (0)