Why six inputs fail
| Approach | SMS autofill | Paste | Backspace |
|---|---|---|---|
| Six TextInputs, maxLength 1 | Breaks | First box only | Needs manual focus juggling |
| One hidden TextInput + drawn boxes | Works | Works | Works |
| Library (otp-entry, confirmation-code-field) | Works | Works | Works |
The operating system delivers an autofilled or pasted code as one string into one field. Six fields with maxLength={1} each catch the first character and drop the rest. You can patch this by catching the string in the first box and splitting it out, then manage focus jumps forward on type and backward on backspace, but every patch adds an edge case. One input that owns the whole code has none of them.
The pattern: one hidden input, six drawn boxes
import { useRef, useState } from 'react';
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
const LENGTH = 6;
export function OtpInput({ onComplete }: { onComplete: (code: string) => void }) {
const [code, setCode] = useState('');
const inputRef = useRef<TextInput>(null);
const handleChange = (text: string) => {
const digits = text.replace(/\D/g, '').slice(0, LENGTH);
setCode(digits);
if (digits.length === LENGTH) onComplete(digits);
};
return (
<Pressable onPress={() => inputRef.current?.focus()} accessible={false}>
<View style={styles.row}>
{Array.from({ length: LENGTH }, (_, i) => {
const active = i === Math.min(code.length, LENGTH - 1);
return (
<View key={i} style={[styles.box, active && styles.boxActive]}>
<Text style={styles.digit}>{code[i] ?? ''}</Text>
</View>
);
})}
</View>
<TextInput
ref={inputRef}
value={code}
onChangeText={handleChange}
maxLength={LENGTH}
keyboardType="number-pad"
textContentType="oneTimeCode" // iOS: code from Messages above keyboard
autoComplete="sms-otp" // Android: keyboard suggests the code
autoFocus
accessibilityLabel="Verification code"
style={styles.hidden}
/>
</Pressable>
);
}
const styles = StyleSheet.create({
row: { flexDirection: 'row', gap: 8, justifyContent: 'center' },
box: { width: 46, height: 56, borderRadius: 10, borderWidth: 1.5, borderColor: '#3a3a3a',
alignItems: 'center', justifyContent: 'center' },
boxActive: { borderColor: '#fb923c' },
digit: { fontSize: 24, color: '#fff', fontVariant: ['tabular-nums'] },
hidden: { position: 'absolute', opacity: 0, width: 1, height: 1 },
});The real TextInput is invisible and holds the whole code. The boxes are just a picture of its value. Tapping anywhere on the boxes focuses the input. Because there is one field, the OS autofill lands in it, paste lands in it, and backspace removes the last digit, with no focus logic at all. Stripping non-digits in handleChangealso cleans up pastes like “Your code is 482 913”.
Keep the hidden input at a tiny non-zero size rather than display: none. A field that is not laid out cannot take focus, and some Android keyboards will not show suggestions for it.
Autofill: what each platform actually does
- iOS.
textContentType="oneTimeCode"makes the code from a recent SMS (or email in Mail) appear in the QuickType bar above the keyboard. One tap fills it. No permission and no special SMS format are needed, though messages that say “code” near the number are detected more reliably. - Android, keyboard suggestion.
autoComplete="sms-otp"lets keyboards that support it, such as Gboard, suggest the code from an incoming SMS. Works in Expo Go. - Android, fully automatic.The SMS Retriever API reads the code with no tap at all, but the SMS must end with your app's hash string and you need a native module, so a development build. Worth it for apps where sign-in happens often; overkill for most.
Wiring it to verification
With Supabase phone auth, the screen calls signInWithOtp on the previous step and verifyOtp when the code is complete:
<OtpInput
onComplete={async (token) => {
setVerifying(true);
const { error } = await supabase.auth.verifyOtp({ phone, token, type: 'sms' });
setVerifying(false);
if (error) setError('That code did not work. Check it or request a new one.');
}}
/>Three details users notice: submit automatically on the sixth digit instead of making them find a button, clear the code and refocus on an error so they can retype immediately, and show a resend link with a visible countdown (30 to 60 seconds) so they do not hammer it. Email codes work the same way with type: 'email'.
Build it or install it?
react-native-otp-entry and react-native-confirmation-code-field both use the single-input approach, are pure JavaScript, and add focus animations and caret styling. Pick one if you want that polish without writing it. If the component above already matches your design, it is fewer moving parts than a dependency you will need to update.
Phone sign-in is a whole flow, not one input: country code, rate limits, resend, error states. Describe it to ShipNative, “phone login with a 6-digit SMS code that autofills, resend after 30 seconds, Supabase auth”, and it generates the screens and the auth calls as real React Native that you can test on your own phone with a real SMS.