The basic controlled input
import { useState } from 'react';
import { StyleSheet, Text, TextInput, View } from 'react-native';
export default function NameField() {
const [name, setName] = useState('');
return (
<View style={styles.wrap}>
<Text style={styles.label}>Name</Text>
<TextInput
value={name}
onChangeText={setName}
placeholder="Ada Lovelace"
placeholderTextColor="#5a5a5a"
autoCapitalize="words"
style={styles.input}
/>
</View>
);
}
const styles = StyleSheet.create({
wrap: { marginBottom: 20 },
label: { color: '#8a8a8a', fontSize: 13, marginBottom: 8 },
input: {
height: 52,
paddingHorizontal: 16,
paddingVertical: 0, // Android adds its own otherwise
borderRadius: 12,
backgroundColor: '#141414',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.08)',
color: '#fff',
fontSize: 16,
},
});Two details web developers miss. First, onChangeText gives you the string directly, so there is no e.target.value. Second, there is no default styling worth keeping: no border, no background, and a text colour that may be black on your dark screen. Always set color and placeholderTextColor explicitly, or the field is invisible in dark mode on one platform and fine on the other.
Keep fontSize at 16 or above for inputs. It reads better on a phone, and it matches what users expect from every other app on the device.
The right props for each field type
There is no type="email". The keyboard, autofill, and capitalisation are separate props, and getting them right is most of what makes a form feel native. Since React Native 0.71, inputMode is the cross-platform way to pick the keyboard and replaces most uses of keyboardType.
| Field | Props | What it buys you |
|---|---|---|
inputMode="email" autoCapitalize="none" autoComplete="email" textContentType="emailAddress" | Shows @ on the keyboard and stops the first letter being capitalised | |
| Password (log in) | secureTextEntry autoComplete="password" textContentType="password" | Lets the OS password manager offer saved credentials |
| Password (sign up) | secureTextEntry textContentType="newPassword" | iOS offers a strong generated password instead of a saved one |
| Phone | inputMode="tel" autoComplete="tel" textContentType="telephoneNumber" | Phone pad plus autofill from the contact card |
| Amount | inputMode="decimal" | Number pad with a decimal separator for the user locale |
| SMS code | inputMode="numeric" textContentType="oneTimeCode" autoComplete="sms-otp" | iOS and Android suggest the code from the incoming text message |
| Search | inputMode="search" returnKeyType="search" autoCorrect={false} | Return key reads "Search" and autocorrect stops rewriting queries |
textContentType is iOS only and autoComplete drives Android autofill, which is why the login rows set both. Skip them and the form still works, but the password manager never offers to fill it, and that is the difference users notice on day one. The numeric keyboards are a hint, not a guarantee: paste and hardware keyboards ignore them, so still sanitise in onChangeText.
Moving to the next field with the return key
import { useRef, useState } from 'react';
import { TextInput } from 'react-native';
export default function LoginForm({ onSubmit }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const passwordRef = useRef<TextInput>(null);
return (
<>
<TextInput
value={email}
onChangeText={setEmail}
inputMode="email"
autoCapitalize="none"
autoComplete="email"
textContentType="emailAddress"
returnKeyType="next"
submitBehavior="submit" // keep the keyboard up
onSubmitEditing={() => passwordRef.current?.focus()}
/>
<TextInput
ref={passwordRef}
value={password}
onChangeText={setPassword}
secureTextEntry
autoComplete="password"
textContentType="password"
returnKeyType="go"
onSubmitEditing={() => onSubmit(email, password)}
/>
</>
);
}Without submitBehavior="submit" the keyboard closes on return and reopens a beat later for the next field, which reads as a glitch. On React Native older than 0.73 the same fix is blurOnSubmit={false}. The last field uses returnKeyType="go" and submits, so a user can log in without ever reaching for the button.
Three bugs you only see on a real phone
- The Android box is too tall, or the text sits low. Android TextInput ships with default vertical padding and extra font padding. Set
paddingVerticalyourself and addincludeFontPadding: falseif the text still looks off-centre. For multiline inputs, addtextAlignVertical: 'top'or the first line starts in the middle of the box. Multiline fields have their own quirks, covered in the text area guide. - The cursor jumps to the end while typing. This happens when
onChangeTextrewrites the value, for example inserting spaces into a card number or dashes into a phone number. React Native sets the new string and the caret lands at the end, so editing the middle of the value becomes impossible. Format on blur, or show the formatted version in a separate Text and keep the input raw. - Typing lags on a busy screen. A controlled input re-renders its owner on every keystroke. If the owner is a whole screen with a list and images, each letter re-renders all of it. Move the input and its state into a small component that reports upward on submit or blur, or use
defaultValueand a ref for long free-text fields.
The fourth bug is not really a TextInput bug: the keyboard covers the field you are typing into. That one has its own fix in the KeyboardAvoidingView guide.
Should you use a form library?
For two or three fields, plain useStateis clearer than any library. Past about five fields, or once you have rules like “confirm password must match”, React Hook Form with a schema validator saves real code. It wraps TextInput through its Controller component and keeps re-renders scoped to the field that changed, which also solves the lag problem above. The form validation guide walks through that setup.
Keyboard behaviour is the one part of a form you cannot judge in a browser. If you want to see whether the right keyboards, autofill, and return-key chain are in place, describe the screen, “a sign-up form with name, email, phone, and password”, and ShipNative builds it as real React Native that runs on your phone, where you can tap through it with the actual keyboard.