OTP Input
The OtpInput
component is a specialized input field designed for capturing single digits in OTP (One-Time Password) verification flows. It automatically validates numeric input, handles backspace navigation, and provides a consistent visual design for verification codes.
Import
Section titled “Import”import { OtpInput } from '@space-uy/pulsar-ui';
Basic usage
Section titled “Basic usage”<OtpInput placeholder="*" onChangeText={(digit) => console.log(digit)} />
Properties
Section titled “Properties”Property | Type | Required | Default value | Description |
---|---|---|---|---|
placeholder | string | ❌ | '*' | Placeholder character when input is empty |
onChangeText | (text: string) => void | ❌ | - | Callback when input text changes |
onKeyPress | (event: NativeSyntheticEvent<TextInputKeyPressEventData>) => void | ❌ | - | Callback for key press events |
editable | boolean | ❌ | true | Whether the input is editable |
style | StyleProp<ViewStyle> | ❌ | - | Custom styles for the input |
...rest | TextInputProps | ❌ | - | Additional TextInput props |
Basic examples
Section titled “Basic examples”Single OTP input
Section titled “Single OTP input”const [digit, setDigit] = useState('');
<OtpInput placeholder="0" onChangeText={setDigit} value={digit} />;
Complete OTP form
Section titled “Complete OTP form”const [otpCode, setOtpCode] = useState(['', '', '', '', '', '']);const inputRefs = useRef<Array<TextInput | null>>([]);
const handleOtpChange = (value: string, index: number) => { const newOtpCode = [...otpCode]; newOtpCode[index] = value; setOtpCode(newOtpCode);
// Auto-focus next input if (value && index < 5) { inputRefs.current[index + 1]?.focus(); }};
const handleKeyPress = (event: any, index: number) => { if (event.nativeEvent.key === 'Backspace' && !otpCode[index] && index > 0) { inputRefs.current[index - 1]?.focus(); }};
<View style={{ flexDirection: 'row', gap: 12, justifyContent: 'center' }}> {otpCode.map((digit, index) => ( <OtpInput key={index} ref={(ref) => (inputRefs.current[index] = ref)} placeholder="0" value={digit} onChangeText={(value) => handleOtpChange(value, index)} onKeyPress={(event) => handleKeyPress(event, index)} /> ))}</View>;
Advanced examples
Section titled “Advanced examples”Verification flow with validation
Section titled “Verification flow with validation”const [otpCode, setOtpCode] = useState(['', '', '', '', '', '']);const [isValid, setIsValid] = useState(true);const [isLoading, setIsLoading] = useState(false);const inputRefs = useRef<Array<TextInput | null>>([]);
const handleOtpChange = (value: string, index: number) => { const newOtpCode = [...otpCode]; newOtpCode[index] = value; setOtpCode(newOtpCode); setIsValid(true); // Reset validation state
// Auto-focus next input if (value && index < 5) { inputRefs.current[index + 1]?.focus(); }
// Auto-submit when complete if (index === 5 && value) { handleVerification(newOtpCode); }};
const handleVerification = async (code: string[]) => { const codeString = code.join(''); if (codeString.length === 6) { setIsLoading(true); try { await verifyOtpCode(codeString); // Handle success } catch (error) { setIsValid(false); setOtpCode(['', '', '', '', '', '']); inputRefs.current[0]?.focus(); } finally { setIsLoading(false); } }};
<View style={{ alignItems: 'center', gap: 20 }}> <Text variant="h4">Enter verification code</Text> <Text variant="pm" style={{ textAlign: 'center', opacity: 0.7 }}> We sent a 6-digit code to {maskedPhone} </Text>
<View style={{ flexDirection: 'row', gap: 12 }}> {otpCode.map((digit, index) => ( <OtpInput key={index} ref={(ref) => (inputRefs.current[index] = ref)} placeholder="0" value={digit} onChangeText={(value) => handleOtpChange(value, index)} onKeyPress={(event) => handleKeyPress(event, index)} editable={!isLoading} style={{ borderColor: !isValid ? colors.destructive : colors.border, backgroundColor: isLoading ? colors.altBackground : colors.background, }} /> ))} </View>
{!isValid && ( <Text variant="caption" style={{ color: colors.destructive }}> Invalid code. Please try again. </Text> )}
{isLoading && ( <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}> <LoadingIndicator size={16} /> <Text variant="pm">Verifying...</Text> </View> )}</View>;
Resend code functionality
Section titled “Resend code functionality”const [otpCode, setOtpCode] = useState(['', '', '', '', '', '']);const [countdown, setCountdown] = useState(60);const [canResend, setCanResend] = useState(false);
useEffect(() => { let timer: NodeJS.Timeout; if (countdown > 0) { timer = setTimeout(() => setCountdown(countdown - 1), 1000); } else { setCanResend(true); } return () => clearTimeout(timer);}, [countdown]);
const handleResendCode = async () => { if (!canResend) return;
try { await resendOtpCode(); setCountdown(60); setCanResend(false); setOtpCode(['', '', '', '', '', '']); inputRefs.current[0]?.focus(); } catch (error) { // Handle error }};
<View style={{ alignItems: 'center', gap: 20 }}> <View style={{ flexDirection: 'row', gap: 12 }}> {otpCode.map((digit, index) => ( <OtpInput key={index} ref={(ref) => (inputRefs.current[index] = ref)} placeholder="0" value={digit} onChangeText={(value) => handleOtpChange(value, index)} /> ))} </View>
<View style={{ alignItems: 'center', gap: 8 }}> <Text variant="pm">Didn't receive the code?</Text> <Button text={canResend ? 'Resend code' : `Resend in ${countdown}s`} variant="transparent" onPress={handleResendCode} disabled={!canResend} /> </View></View>;
Custom styled OTP inputs
Section titled “Custom styled OTP inputs”const [otpCode, setOtpCode] = useState(['', '', '', '']);
<View style={{ alignItems: 'center', gap: 16 }}> <Text variant="h4">Security Code</Text>
<View style={{ flexDirection: 'row', gap: 16 }}> {otpCode.map((digit, index) => ( <OtpInput key={index} placeholder="•" value={digit} onChangeText={(value) => handleOtpChange(value, index)} style={{ width: 60, height: 60, borderRadius: 12, borderWidth: 2, borderColor: digit ? colors.primary : colors.border, backgroundColor: digit ? colors.primary + '10' : colors.background, fontSize: 24, fontWeight: 'bold', }} /> ))} </View>
<Text variant="caption" style={{ textAlign: 'center', opacity: 0.7 }}> Enter the 4-digit code from your authenticator app </Text></View>;
Two-factor authentication
Section titled “Two-factor authentication”const [authMethod, setAuthMethod] = useState<'sms' | 'app'>('sms');const [otpCode, setOtpCode] = useState(['', '', '', '', '', '']);
<Card> <Text variant="h4">Two-Factor Authentication</Text>
<View style={{ marginTop: 16, gap: 16 }}> <View style={{ flexDirection: 'row', gap: 12 }}> <Chip text="SMS Code" onPress={() => setAuthMethod('sms')} style={{ backgroundColor: authMethod === 'sms' ? colors.primary : colors.altBackground, }} /> <Chip text="Authenticator App" onPress={() => setAuthMethod('app')} style={{ backgroundColor: authMethod === 'app' ? colors.primary : colors.altBackground, }} /> </View>
<Text variant="pm" style={{ opacity: 0.7 }}> {authMethod === 'sms' ? 'Enter the 6-digit code sent to your phone' : 'Enter the 6-digit code from your authenticator app'} </Text>
<View style={{ flexDirection: 'row', gap: 8, justifyContent: 'center' }}> {otpCode.map((digit, index) => ( <OtpInput key={index} placeholder="0" value={digit} onChangeText={(value) => handleOtpChange(value, index)} /> ))} </View> </View></Card>;
Implementation notes
Section titled “Implementation notes”- Input automatically focuses next field when a digit is entered
- Backspace navigation moves to previous field when current is empty
- Only numeric input is accepted (0-9)
- Paste operations are handled gracefully to fill multiple fields
- The component maintains consistent styling with the design system
Validation Features
Section titled “Validation Features”- Numeric Only: Automatically filters non-numeric input
- Single Character: Prevents entering more than one character
- Auto-focus: Seamless navigation between input fields
- Paste Support: Intelligent handling of pasted OTP codes
- Error States: Visual feedback for invalid codes
Accessibility
Section titled “Accessibility”- Each input is properly labeled for screen readers
- Keyboard navigation works seamlessly between fields
- Focus management ensures smooth user experience
- High contrast support for better visibility
- Proper text input type for mobile keyboards