@shubhh6003/react-native-otp-input
v0.1.0
Published
Production-quality, accessible, pure-JS OTP and PIN input component for React Native and Expo with SMS autofill, paste support, and full customization.
Maintainers
Readme
@shubhh6003/react-native-otp-input
A polished, accessible, zero-dependency, pure-JS OTP and PIN input component for React Native and Expo with SMS autofill, paste support, and full customization.
Demo
| iOS | Android |
| :---: | :---: |
|
|
|
(Replace with recorded GIFs in your GitHub repository)
Features
- ⚡ Zero Native Code: Pure JavaScript/TypeScript. Works seamlessly across iOS, Android, and Expo (both managed and bare workflows).
- 📦 Zero Runtime Dependencies: Only requires
reactandreact-nativeas peer dependencies. - 📱 Native SMS Autofill: Supports iOS One-Time Code (
textContentType="oneTimeCode") and Android SMS OTP (autoComplete="sms-otp"). - 📋 Seamless Paste Support: Paste formatted codes (e.g.
123 456,12-34-56, or123456) directly into the input. Automatically strips delimiters and truncates overflow. - ♿ Accessible by Design: Decorative visual boxes hidden from screen readers, while the hidden
TextInputprovides full accessibility hints and announcements. - 🔒 PIN & Secure Mode: Mask characters with custom mask symbols (
•,●,*) and optional alphanumeric support. - 🔄 Controlled & Uncontrolled: Effortlessly switch between controlled
valueor uncontrolleddefaultValue. - ⏱️ Countdown Hook Included: Comes with
useOtpTimerfor self-cleaning "Resend code in 30s" timers. - 🎨 Fully Customizable: Flexible style props for containers, active boxes, filled boxes, error states, and text typography.
Installation
# npm
npm install @shubhh6003/react-native-otp-input
# yarn
yarn add @shubhh6003/react-native-otp-input
# expo
npx expo install @shubhh6003/react-native-otp-inputQuick Start (Controlled)
import React, { useState } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import { OtpInput } from '@shubhh6003/react-native-otp-input';
export default function VerificationScreen() {
const [code, setCode] = useState('');
return (
<View style={styles.container}>
<Text style={styles.title}>Enter Verification Code</Text>
<OtpInput
length={6}
value={code}
onChange={setCode}
onComplete={completedCode => {
console.log('OTP verified:', completedCode);
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
title: {
fontSize: 20,
fontWeight: '600',
marginBottom: 20,
},
});Props Reference
| Prop | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| length | number | 6 | Number of boxes (allowed 3 to 8). |
| value | string | undefined | Controlled value. |
| defaultValue | string | '' | Initial value for uncontrolled usage. |
| onChange | (code: string) => void | undefined | Callback fired on every change. |
| onComplete | (code: string) => void | undefined | Callback fired once when all boxes are filled. |
| autoFocus | boolean | true | Auto-focus the input on mount. |
| type | 'numeric' \| 'alphanumeric' | 'numeric' | Allowed characters; 'numeric' uses keyboardType="number-pad". |
| secureTextEntry | boolean | false | Mask characters for PIN/password input. |
| maskChar | string | '•' | Character displayed when secureTextEntry is true. |
| placeholder | string | '' | Placeholder character shown in empty boxes (e.g. '-'). |
| disabled | boolean | false | Disables user interaction and dims the boxes. |
| error | boolean | false | Applies error styling to all boxes. |
| errorMessage | string | undefined | Optional error message text displayed below the boxes. |
| showCursor | boolean | true | Blinking caret shown in the active empty box. |
| separatorAfter | number[] | [] | Inserts a separator after specified box positions (e.g. [3] makes 123 - 456). |
| renderSeparator | (index?: number) => ReactNode | undefined | Custom separator renderer function. |
| autoSubmit | boolean | true | Calls onComplete automatically when code is full. |
| blurOnComplete | boolean | false | Automatically dismisses the keyboard when complete. |
| clearOnError | boolean | false | Clears the input value when error becomes true. |
| containerStyle | StyleProp<ViewStyle> | undefined | Custom style for the root container. |
| boxStyle | StyleProp<ViewStyle> | undefined | Custom style for each individual box. |
| activeBoxStyle | StyleProp<ViewStyle> | undefined | Custom style for the active (focused) box. |
| filledBoxStyle | StyleProp<ViewStyle> | undefined | Custom style for boxes containing a character. |
| errorBoxStyle | StyleProp<ViewStyle> | undefined | Custom style for boxes in error state. |
| textStyle | StyleProp<TextStyle> | undefined | Custom typography style for the character text. |
| cursorStyle | StyleProp<ViewStyle> | undefined | Custom style for the blinking cursor caret. |
| errorTextStyle | StyleProp<TextStyle> | undefined | Custom style for the error message text. |
| testID | string | undefined | Base testID for automated testing. |
| accessibilityLabel | string | 'One time password' | Accessibility label for screen readers. |
| onFocus | () => void | undefined | Callback fired when the input gains focus. |
| onBlur | () => void | undefined | Callback fired when the input loses focus. |
Imperative Ref API
You can access imperative methods using a standard React ref:
import React, { useRef } from 'react';
import { Button } from 'react-native';
import { OtpInput, type OtpInputRef } from '@shubhh6003/react-native-otp-input';
export function OtpControl() {
const otpRef = useRef<OtpInputRef>(null);
return (
<>
<OtpInput ref={otpRef} length={6} />
<Button title="Focus" onPress={() => otpRef.current?.focus()} />
<Button title="Clear" onPress={() => otpRef.current?.clear()} />
<Button title="Fill 654321" onPress={() => otpRef.current?.setValue('654321')} />
<Button title="Log Value" onPress={() => console.log(otpRef.current?.getValue())} />
</>
);
}Ref Methods
export type OtpInputRef = {
focus: () => void;
blur: () => void;
clear: () => void;
setValue: (code: string) => void;
getValue: () => string;
};SMS Autofill Guide
This package utilizes the single hidden TextInput architecture, ensuring that native OS autofill integrates seamlessly without native bridges.
iOS One-Time Code Autofill
- How it works: On iOS 12+, when an incoming SMS contains an authentication code, iOS displays a QuickType suggestion bar above the software keyboard with the code.
- TextInput props applied:
textContentType="oneTimeCode" - Requirements:
- The incoming SMS should contain phrases such as
"verification code"or"code"alongside the digits. - The QuickType keyboard must be enabled in iOS Settings (
Settings > General > Keyboards > Predictive).
- The incoming SMS should contain phrases such as
Android Google Play Services Autofill
- How it works: On Android 8.0+, Google Play Services provides SMS OTP autofill suggestions above the keyboard.
- TextInput props applied:
autoComplete="sms-otp",importantForAutofill="yes". - Note on Secure Mode: Enabling
secureTextEntry={true}tells both iOS and Android that the field is sensitive/private, which may cause the OS to suppress SMS autofill suggestions. If you require autofill, keepsecureTextEntry={false}.
Optional: Combining with SMS Retriever API
For zero-tap automatic SMS verification on Android, you can integrate with the Google SMS Retriever API using an external library such as react-native-otp-verify without adding native code to this component:
import React, { useEffect, useRef } from 'react';
import RNOtpVerify from 'react-native-otp-verify';
import { OtpInput, type OtpInputRef } from '@shubhh6003/react-native-otp-input';
export function SmsRetrieverExample() {
const otpRef = useRef<OtpInputRef>(null);
useEffect(() => {
RNOtpVerify.getOtp()
.then(() => RNOtpVerify.addListener(message => {
// Extract 6-digit code using regex
const match = /(\d{6})/.exec(message);
if (match && match[1]) {
otpRef.current?.setValue(match[1]);
}
}))
.catch(console.error);
return () => {
RNOtpVerify.removeListener();
};
}, []);
return <OtpInput ref={otpRef} length={6} />;
}Examples
1. 4-Digit Secure PIN
<OtpInput
length={4}
secureTextEntry={true}
maskChar="●"
onComplete={pin => verifyPin(pin)}
/>2. Alphanumeric Voucher / Promo Code
<OtpInput
length={5}
type="alphanumeric"
placeholder="-"
onChange={code => setVoucher(code)}
/>3. Grouped with Separators (3 + 3)
<OtpInput
length={6}
separatorAfter={[3]}
onChange={code => setCode(code)}
/>4. Error State with Auto-Clear
<OtpInput
length={6}
error={isInvalid}
errorMessage={isInvalid ? 'Invalid code. Please try again.' : undefined}
clearOnError={true}
onChange={() => setIsInvalid(false)}
/>5. Resend Countdown Timer (useOtpTimer)
import React from 'react';
import { View, Button, Text } from 'react-native';
import { OtpInput, useOtpTimer } from '@shubhh6003/react-native-otp-input';
export function ResendExample() {
const { secondsLeft, isRunning, restart } = useOtpTimer(30);
return (
<View>
<OtpInput length={6} />
<Button
title={isRunning ? `Resend code in ${secondsLeft}s` : 'Resend Code'}
disabled={isRunning}
onPress={() => restart(30)}
/>
</View>
);
}6. Custom Rounded / Circular Style
<OtpInput
length={4}
boxStyle={{
width: 56,
height: 56,
borderRadius: 28,
borderWidth: 2,
borderColor: '#E2E8F0',
backgroundColor: '#F8FAFC',
}}
activeBoxStyle={{
borderColor: '#7C3AED',
backgroundColor: '#F5F3FF',
}}
textStyle={{
fontSize: 22,
fontWeight: '700',
color: '#6D28D9',
}}
/>7. Underline-Only Style
<OtpInput
length={4}
boxStyle={{
width: 48,
height: 56,
borderRadius: 0,
borderWidth: 0,
borderBottomWidth: 3,
borderBottomColor: '#CBD5E1',
backgroundColor: 'transparent',
}}
activeBoxStyle={{
borderBottomColor: '#059669',
}}
textStyle={{
fontSize: 24,
color: '#065F46',
}}
/>Accessibility
- Decorative visual boxes: Wrapped with
importantForAccessibility="no-hide-descendants"andaccessibilityElementsHidden={true}to prevent screen readers from reading each individual box repeatedly. - Accessible hidden input: Contains descriptive
accessibilityLabel(default:'One time password') andaccessibilityHintannouncing the total number of characters required. - Live region announcements: Uses
AccessibilityInfo.announceForAccessibilityto alert users of errors and successful input completion. - Font scaling safeguard: Applies
allowFontScaling={false}to individual box text to preserve the touch-target layout while maintaining readable dimensions.
Troubleshooting & FAQ
Q: Why does SMS autofill not show up on iOS?
- Ensure your test SMS message includes keywords like
codeorverification code(e.g."Your verification code is 123456"). - Check that QuickType / Predictive text is enabled under iOS
Settings > General > Keyboards > Predictive. - If
secureTextEntry={true}is enabled, iOS treats the input as confidential and disables autofill suggestions.
Q: Why does autoFocus sometimes fail on Android?
On certain Android devices and custom ROMs, focusing TextInput immediately during mount can be ignored if the activity window hasn't completed its layout transition. This library incorporates a small delayed requestAnimationFrame on mount to ensure reliable autofocus across Android versions.
Q: How does paste support work?
The component captures pasted strings via the hidden TextInput. Any non-allowed characters, formatting spaces, or hyphens (e.g. 123 456 or 12-34-56) are automatically sanitized and truncated to the specified length.
Q: Why is my keyboard closing automatically?
Ensure blurOnComplete is set to false (default) if you wish to keep the keyboard visible after the user completes entering all digits.
Contributing
Contributions are welcome! Please read CONTRIBUTING.md and CODE_OF_CONDUCT.md for details on submitting pull requests and running tests.
License
MIT © Shubh
