⌨️ TextInput: Capturing User Input
Building forms and text fields that feel native on every platform
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Create controlled text inputs with proper state management
- Choose appropriate keyboard types for different input scenarios
- Implement secure text entry for passwords
- Build multiline text areas for longer content
- Manage focus between multiple inputs
- Dismiss the keyboard gracefully
- Handle platform-specific styling differences
⏱️ Estimated Time: 30-40 minutes
📑 In This Lesson
Introduction to TextInput
Every app needs to collect user input at some point — usernames, passwords, search queries, messages, comments. In React Native, the TextInput component is your gateway to capturing text from users.
📖 What is TextInput?
TextInput is a core React Native component that allows users to enter text via the device keyboard. It's the equivalent of <input type="text"> and <textarea> combined, but with mobile-specific features like keyboard types and return key behaviors.
Web vs Native Text Inputs
🌐 Web Input
<input
type="text"
placeholder="Enter name"
value={name}
onChange={e => setName(e.target.value)}
/>
📱 React Native
<TextInput
placeholder="Enter name"
value={name}
onChangeText={setName}
/>
Key Differences from Web
| Feature | Web | React Native |
|---|---|---|
| Change handler | onChange (event object) |
onChangeText (string directly) |
| Submit handler | Form onSubmit |
onSubmitEditing |
| Keyboard control | Browser handles | You control dismissal |
| Multiline | <textarea> |
multiline={true} |
| Default styling | Browser default | Almost none! |
Basic TextInput
import { useState } from 'react';
import { TextInput, View, Text, StyleSheet } from 'react-native';
function BasicInput() {
const [text, setText] = useState('');
return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="Type something..."
value={text}
onChangeText={setText}
/>
<Text style={styles.preview}>You typed: {text}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 20,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
},
preview: {
marginTop: 16,
fontSize: 14,
color: '#666',
},
});
⚠️ TextInput Has No Default Styling
Unlike web inputs that come with browser styling, React Native's TextInput is essentially invisible by default. You must add your own border, padding, and background to make it visible and usable.
Controlled vs Uncontrolled Inputs
Just like in React for web, you can use TextInput in controlled or uncontrolled mode. In React Native, controlled inputs are strongly recommended.
Controlled Input (Recommended)
The component state is the "single source of truth" for the input value:
import { useState } from 'react';
import { TextInput, StyleSheet } from 'react-native';
function ControlledInput() {
const [email, setEmail] = useState('');
return (
<TextInput
style={styles.input}
placeholder="Enter email"
value={email} // Controlled by state
onChangeText={setEmail} // Updates state on every keystroke
keyboardType="email-address"
autoCapitalize="none"
/>
);
}
Why Controlled is Better
✅ Validation
Validate and transform input as user types (e.g., format phone numbers, limit characters).
✅ Conditional Logic
Show/hide UI based on input value, enable/disable buttons, etc.
✅ Easy Reset
Clear form by simply setting state to empty string.
✅ Predictable
Input always reflects state — no sync issues.
Real-Time Validation Example
import { useState } from 'react';
import { TextInput, View, Text, StyleSheet } from 'react-native';
function EmailInput() {
const [email, setEmail] = useState('');
const [isValid, setIsValid] = useState(true);
const validateEmail = (text: string) => {
setEmail(text);
// Simple email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
setIsValid(text === '' || emailRegex.test(text));
};
return (
<View>
<TextInput
style={[
styles.input,
!isValid && styles.inputError
]}
placeholder="Enter email"
value={email}
onChangeText={validateEmail}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
{!isValid && (
<Text style={styles.errorText}>Please enter a valid email</Text>
)}
</View>
);
}
const styles = StyleSheet.create({
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
},
inputError: {
borderColor: '#f44336',
backgroundColor: '#ffebee',
},
errorText: {
color: '#f44336',
fontSize: 12,
marginTop: 4,
},
});
Input Transformation
You can transform input as the user types:
// Force uppercase
const handleUsernameChange = (text: string) => {
setUsername(text.toUpperCase());
};
// Limit to numbers only
const handlePhoneChange = (text: string) => {
const numbersOnly = text.replace(/[^0-9]/g, '');
setPhone(numbersOnly);
};
// Format as phone number
const handlePhoneFormat = (text: string) => {
const numbers = text.replace(/[^0-9]/g, '');
if (numbers.length <= 3) {
setPhone(numbers);
} else if (numbers.length <= 6) {
setPhone(`(${numbers.slice(0, 3)}) ${numbers.slice(3)}`);
} else {
setPhone(`(${numbers.slice(0, 3)}) ${numbers.slice(3, 6)}-${numbers.slice(6, 10)}`);
}
};
// Limit character count
const handleBioChange = (text: string) => {
if (text.length <= 150) {
setBio(text);
}
};
flowchart LR
A[User Types] --> B[onChangeText fires]
B --> C[setState called]
C --> D[Component re-renders]
D --> E[TextInput displays
new value prop]
E --> A
style A fill:#e3f2fd,stroke:#1976d2
style C fill:#fff3e0,stroke:#ff9800
style E fill:#e8f5e9,stroke:#4caf50
The controlled input cycle: user types → state updates → input re-renders
Uncontrolled Input (Rare Use Cases)
You can use defaultValue instead of value for uncontrolled behavior, but this is rare:
import { useRef } from 'react';
import { TextInput, Button, View } from 'react-native';
function UncontrolledInput() {
const inputRef = useRef<TextInput>(null);
const handleSubmit = () => {
// Can't easily access current value!
// Would need native module or workaround
};
return (
<View>
<TextInput
ref={inputRef}
defaultValue="Initial value" // Uncontrolled
placeholder="Type here"
/>
<Button title="Submit" onPress={handleSubmit} />
</View>
);
}
// ❌ Generally avoid - harder to work with in React Native
Keyboard Types
One of the best parts of native development is showing the right keyboard for the job. React Native's keyboardType prop lets you customize the keyboard layout.
Different keyboard types optimize for specific input scenarios
Available Keyboard Types
| keyboardType | Use Case | Platform |
|---|---|---|
default |
General text input | Both |
email-address |
Email with @ and . keys prominent | Both |
numeric |
Numbers with punctuation | Both |
phone-pad |
Phone numbers (numbers + * #) | Both |
number-pad |
Whole numbers only | Both |
decimal-pad |
Numbers with decimal point | Both |
url |
URLs with /, ., and Go button | Both |
web-search |
Search with Search button | Both |
visible-password |
Text without autocomplete | Android |
ascii-capable |
ASCII characters only | iOS |
Keyboard Type Examples
import { TextInput, View, Text, StyleSheet } from 'react-native';
import { useState } from 'react';
function FormWithKeyboardTypes() {
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [age, setAge] = useState('');
const [price, setPrice] = useState('');
const [website, setWebsite] = useState('');
return (
<View style={styles.form}>
{/* Email - shows @ key prominently */}
<Text style={styles.label}>Email</Text>
<TextInput
style={styles.input}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
placeholder="you@example.com"
value={email}
onChangeText={setEmail}
/>
{/* Phone - numeric with phone symbols */}
<Text style={styles.label}>Phone</Text>
<TextInput
style={styles.input}
keyboardType="phone-pad"
placeholder="(555) 123-4567"
value={phone}
onChangeText={setPhone}
/>
{/* Age - whole numbers only */}
<Text style={styles.label}>Age</Text>
<TextInput
style={styles.input}
keyboardType="number-pad"
placeholder="25"
value={age}
onChangeText={setAge}
maxLength={3}
/>
{/* Price - numbers with decimal */}
<Text style={styles.label}>Price</Text>
<TextInput
style={styles.input}
keyboardType="decimal-pad"
placeholder="9.99"
value={price}
onChangeText={setPrice}
/>
{/* Website - URL keyboard */}
<Text style={styles.label}>Website</Text>
<TextInput
style={styles.input}
keyboardType="url"
autoCapitalize="none"
autoCorrect={false}
placeholder="https://example.com"
value={website}
onChangeText={setWebsite}
/>
</View>
);
}
const styles = StyleSheet.create({
form: {
padding: 20,
},
label: {
fontSize: 14,
fontWeight: '600',
marginBottom: 4,
marginTop: 16,
color: '#333',
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
});
💡 Keyboard Type Tips
- Always pair
email-addresswithautoCapitalize="none" - Use
decimal-padfor currency/prices,number-padfor counts phone-padstill allows non-numeric input — validate manually- The keyboard is a hint to the system, not a restriction on input
Secure Entry and Multiline
Two essential features you'll use frequently: hiding password characters and allowing multi-line text entry.
Secure Text Entry (Passwords)
Use secureTextEntry to hide characters as the user types — essential for passwords:
import { useState } from 'react';
import { TextInput, View, Pressable, Text, StyleSheet } from 'react-native';
function PasswordInput() {
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
return (
<View style={styles.container}>
<View style={styles.inputWrapper}>
<TextInput
style={styles.input}
placeholder="Enter password"
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword} // Toggle visibility
autoCapitalize="none"
autoCorrect={false}
textContentType="password" // iOS autofill hint
/>
<Pressable
onPress={() => setShowPassword(!showPassword)}
style={styles.toggleButton}
>
<Text style={styles.toggleText}>
{showPassword ? '🙈' : '👁️'}
</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 20,
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
backgroundColor: '#fff',
},
input: {
flex: 1,
padding: 12,
fontSize: 16,
},
toggleButton: {
padding: 12,
},
toggleText: {
fontSize: 20,
},
});
textContentType for iOS Autofill
iOS can autofill from Keychain. Use textContentType to hint what kind of data you're requesting:
// Username field
<TextInput
textContentType="username"
autoComplete="username"
/>
// Password field
<TextInput
textContentType="password"
autoComplete="password"
secureTextEntry
/>
// New password (signup)
<TextInput
textContentType="newPassword"
autoComplete="password-new"
secureTextEntry
/>
// One-time code (SMS verification)
<TextInput
textContentType="oneTimeCode"
autoComplete="sms-otp"
keyboardType="number-pad"
/>
// Other useful types:
// - emailAddress
// - telephoneNumber
// - name, givenName, familyName
// - streetAddressLine1, city, postalCode
// - creditCardNumber
✅ autoComplete for Android
While textContentType is iOS-specific, use autoComplete for Android autofill. Common values: username, password, email, tel, postal-code, cc-number.
Multiline Text Input
For text areas (comments, bios, messages), add multiline={true}:
import { useState } from 'react';
import { TextInput, View, Text, StyleSheet } from 'react-native';
function BioInput() {
const [bio, setBio] = useState('');
const maxLength = 150;
return (
<View style={styles.container}>
<Text style={styles.label}>Bio</Text>
<TextInput
style={styles.textArea}
placeholder="Tell us about yourself..."
value={bio}
onChangeText={setBio}
multiline={true}
numberOfLines={4} // Android hint for initial height
maxLength={maxLength}
textAlignVertical="top" // Android: start text at top
/>
<Text style={styles.charCount}>
{bio.length}/{maxLength}
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 20,
},
label: {
fontSize: 14,
fontWeight: '600',
marginBottom: 8,
color: '#333',
},
textArea: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
minHeight: 100, // Minimum height
backgroundColor: '#fff',
},
charCount: {
textAlign: 'right',
fontSize: 12,
color: '#666',
marginTop: 4,
},
});
Single-line inputs vs multiline text areas
Multiline Tips
⚠️ Multiline Gotchas
- numberOfLines only sets initial height on Android — use
minHeightfor consistent behavior - textAlignVertical="top" is needed on Android to start text at top
- Return key creates new lines instead of submitting — handle submission differently
- Scrolling happens automatically when content exceeds height
Auto-Growing Text Area
Create a text area that grows with content:
import { useState } from 'react';
import { TextInput, StyleSheet } from 'react-native';
function AutoGrowingTextArea() {
const [text, setText] = useState('');
const [height, setHeight] = useState(40);
return (
<TextInput
style={[styles.input, { height: Math.max(40, height) }]}
placeholder="Start typing..."
value={text}
onChangeText={setText}
multiline
onContentSizeChange={(e) => {
setHeight(e.nativeEvent.contentSize.height);
}}
/>
);
}
const styles = StyleSheet.create({
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
maxHeight: 200, // Cap the growth
},
});
Focus Management
In forms with multiple inputs, users expect to tap "Next" and move to the next field. This requires managing focus programmatically.
Refs for Focus Control
import { useRef, useState } from 'react';
import { TextInput, View, StyleSheet } from 'react-native';
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const passwordRef = useRef<TextInput>(null);
const focusPassword = () => {
passwordRef.current?.focus();
};
const handleSubmit = () => {
console.log('Submitting:', { email, password });
};
return (
<View style={styles.form}>
<TextInput
style={styles.input}
placeholder="Email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
returnKeyType="next" // Shows "Next" on keyboard
onSubmitEditing={focusPassword} // Move to password on submit
blurOnSubmit={false} // Don't dismiss keyboard
/>
<TextInput
ref={passwordRef}
style={styles.input}
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
returnKeyType="done" // Shows "Done" on keyboard
onSubmitEditing={handleSubmit} // Submit form
/>
</View>
);
}
const styles = StyleSheet.create({
form: {
padding: 20,
gap: 12,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
},
});
Return Key Types
The returnKeyType prop changes what the keyboard's return button says and looks like:
| returnKeyType | Appearance | Use Case |
|---|---|---|
done |
Done | Last field in form |
go |
Go | URL or navigation |
next |
Next | Move to next field |
search |
Search | Search input |
send |
Send | Message/chat input |
default |
Return | General purpose |
Multi-Field Form Example
import { useRef, useState } from 'react';
import { TextInput, View, Text, Pressable, StyleSheet, Alert } from 'react-native';
function SignupForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [password, setPassword] = useState('');
const emailRef = useRef<TextInput>(null);
const phoneRef = useRef<TextInput>(null);
const passwordRef = useRef<TextInput>(null);
const handleSubmit = () => {
if (!name || !email || !password) {
Alert.alert('Error', 'Please fill in all required fields');
return;
}
console.log('Signup:', { name, email, phone, password });
};
return (
<View style={styles.form}>
<Text style={styles.label}>Name *</Text>
<TextInput
style={styles.input}
placeholder="John Doe"
value={name}
onChangeText={setName}
autoCapitalize="words"
returnKeyType="next"
onSubmitEditing={() => emailRef.current?.focus()}
blurOnSubmit={false}
/>
<Text style={styles.label}>Email *</Text>
<TextInput
ref={emailRef}
style={styles.input}
placeholder="john@example.com"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
returnKeyType="next"
onSubmitEditing={() => phoneRef.current?.focus()}
blurOnSubmit={false}
/>
<Text style={styles.label}>Phone (optional)</Text>
<TextInput
ref={phoneRef}
style={styles.input}
placeholder="(555) 123-4567"
value={phone}
onChangeText={setPhone}
keyboardType="phone-pad"
returnKeyType="next"
onSubmitEditing={() => passwordRef.current?.focus()}
blurOnSubmit={false}
/>
<Text style={styles.label}>Password *</Text>
<TextInput
ref={passwordRef}
style={styles.input}
placeholder="Minimum 8 characters"
value={password}
onChangeText={setPassword}
secureTextEntry
returnKeyType="done"
onSubmitEditing={handleSubmit}
/>
<Pressable style={styles.button} onPress={handleSubmit}>
<Text style={styles.buttonText}>Create Account</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
form: {
padding: 20,
},
label: {
fontSize: 14,
fontWeight: '600',
color: '#333',
marginBottom: 4,
marginTop: 12,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
button: {
backgroundColor: '#2196F3',
paddingVertical: 14,
borderRadius: 8,
alignItems: 'center',
marginTop: 24,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
});
flowchart LR
A[Name Field] -->|"Next"| B[Email Field]
B -->|"Next"| C[Phone Field]
C -->|"Next"| D[Password Field]
D -->|"Done"| E[Submit Form]
style A fill:#e3f2fd,stroke:#1976d2
style B fill:#e3f2fd,stroke:#1976d2
style C fill:#e3f2fd,stroke:#1976d2
style D fill:#e3f2fd,stroke:#1976d2
style E fill:#e8f5e9,stroke:#4caf50
Focus flows from field to field, then submits on the last one
Focus Events
<TextInput
onFocus={() => {
console.log('Input focused');
// Maybe scroll to input, highlight label, etc.
}}
onBlur={() => {
console.log('Input blurred');
// Validate input, hide helper text, etc.
}}
/>
Keyboard Dismissal Patterns
Unlike web browsers, mobile keyboards don't automatically dismiss when users tap outside an input. You need to handle this yourself.
The Problem
Without keyboard dismissal handling, users get stuck with the keyboard covering content and no way to dismiss it except pressing a button.
Method 1: Keyboard.dismiss()
import { Keyboard, TextInput, View, Pressable, Text } from 'react-native';
function DismissExample() {
return (
<View>
<TextInput placeholder="Type here..." />
<Pressable onPress={() => Keyboard.dismiss()}>
<Text>Dismiss Keyboard</Text>
</Pressable>
</View>
);
}
Method 2: TouchableWithoutFeedback Wrapper
import {
Keyboard,
TouchableWithoutFeedback,
View,
TextInput,
StyleSheet
} from 'react-native';
function DismissOnTapOutside() {
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="Tap outside to dismiss keyboard"
/>
</View>
</TouchableWithoutFeedback>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
},
});
Method 3: KeyboardAvoidingView + Dismissal
import {
Keyboard,
KeyboardAvoidingView,
TouchableWithoutFeedback,
View,
TextInput,
Platform,
StyleSheet
} from 'react-native';
function FormWithKeyboardHandling() {
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.container}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.inner}>
<TextInput
style={styles.input}
placeholder="Email"
/>
<TextInput
style={styles.input}
placeholder="Password"
secureTextEntry
/>
</View>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
inner: {
flex: 1,
padding: 20,
justifyContent: 'center',
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
marginBottom: 12,
},
});
Method 4: ScrollView keyboardDismissMode
import { ScrollView, TextInput, StyleSheet } from 'react-native';
function ScrollFormWithDismiss() {
return (
<ScrollView
style={styles.container}
keyboardDismissMode="on-drag" // Dismiss when scrolling
// Other options: "none", "interactive" (iOS)
keyboardShouldPersistTaps="handled" // Allow tapping buttons
>
<TextInput style={styles.input} placeholder="Field 1" />
<TextInput style={styles.input} placeholder="Field 2" />
<TextInput style={styles.input} placeholder="Field 3" />
{/* More fields... */}
</ScrollView>
);
}
keyboardDismissMode Options
| Mode | Behavior | Best For |
|---|---|---|
none |
Scrolling doesn't dismiss keyboard | When you need keyboard always visible |
on-drag |
Keyboard dismisses when scroll begins | Long forms, feeds |
interactive |
Keyboard dismisses interactively with drag (iOS) | Chat apps, iOS only |
keyboardShouldPersistTaps
This prop controls what happens when you tap while the keyboard is open:
// "never" - Tapping outside dismisses keyboard, button won't trigger
// "always" - Keyboard stays, all taps go through
// "handled" - Keyboard stays for handled touches (buttons), dismisses for unhandled
<ScrollView keyboardShouldPersistTaps="handled">
{/* Tapping a button works, tapping empty space dismisses */}
</ScrollView>
✅ Recommended Pattern
For most forms, use:
<ScrollView
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
>
This allows users to scroll to dismiss, while still being able to tap submit buttons with the keyboard open.
flowchart TD
A[Need keyboard dismissal?] --> B{Screen type?}
B -->|Simple form| C[TouchableWithoutFeedback
+ Keyboard.dismiss]
B -->|Scrollable form| D[ScrollView with
keyboardDismissMode]
B -->|Chat/messaging| E[interactive mode
iOS only]
D --> F{Also need tappable buttons?}
F -->|Yes| G[Add keyboardShouldPersistTaps='handled']
F -->|No| H[Default behavior fine]
style C fill:#e3f2fd,stroke:#1976d2
style D fill:#e3f2fd,stroke:#1976d2
style E fill:#fff3e0,stroke:#ff9800
style G fill:#e8f5e9,stroke:#4caf50
Choose keyboard dismissal pattern based on your screen type
Platform-Specific Styling
TextInput looks and behaves differently on iOS and Android. Understanding these differences helps you create consistent experiences.
Default Differences
iOS and Android have different default TextInput appearances
Normalizing Styles Cross-Platform
import { TextInput, StyleSheet, Platform } from 'react-native';
const styles = StyleSheet.create({
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
// Platform-specific fixes
...Platform.select({
android: {
// Remove the default underline
borderBottomWidth: 0,
// Text starts at top for multiline
textAlignVertical: 'top',
},
ios: {
// iOS specific if needed
},
}),
},
});
Selection and Cursor Colors
<TextInput
// Cursor color (iOS and Android 10+)
cursorColor="#2196F3"
// Selection highlight color
selectionColor="rgba(33, 150, 243, 0.3)"
// Android: tint color for selection handles
selectionHandleColor="#2196F3" // Android 14+ only
/>
Placeholder Styling
<TextInput
placeholder="Enter text..."
placeholderTextColor="#999" // Custom placeholder color
/>
Complete Cross-Platform Input Component
import { TextInput, StyleSheet, Platform, TextInputProps, View, Text } from 'react-native';
interface InputProps extends TextInputProps {
label?: string;
error?: string;
}
function Input({ label, error, style, ...props }: InputProps) {
return (
<View style={styles.container}>
{label && <Text style={styles.label}>{label}</Text>}
<TextInput
style={[
styles.input,
error && styles.inputError,
style,
]}
placeholderTextColor="#999"
cursorColor="#2196F3"
selectionColor="rgba(33, 150, 243, 0.3)"
{...props}
/>
{error && <Text style={styles.errorText}>{error}</Text>}
</View>
);
}
const styles = StyleSheet.create({
container: {
marginBottom: 16,
},
label: {
fontSize: 14,
fontWeight: '600',
color: '#333',
marginBottom: 6,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: Platform.select({ ios: 14, android: 12 }),
fontSize: 16,
backgroundColor: '#fff',
color: '#333',
},
inputError: {
borderColor: '#f44336',
},
errorText: {
color: '#f44336',
fontSize: 12,
marginTop: 4,
},
});
💡 Other Useful Props
- clearButtonMode (iOS): Show clear button — "never", "while-editing", "unless-editing", "always"
- enablesReturnKeyAutomatically (iOS): Disable return key when empty
- importantForAutofill (Android): Hint for autofill — "auto", "yes", "no"
- contextMenuHidden: Hide copy/paste menu
- editable: Set to false to make read-only
- selectTextOnFocus: Select all text when focused
Hands-On Exercises
Time to put TextInput to work! These exercises cover real-world form scenarios.
Exercise 1: Validated Email Input
Goal: Create an email input with real-time validation.
Requirements:
- Email keyboard type and proper capitalization settings
- Validate format on every change
- Show error state with red border when invalid
- Show "✓ Valid email" in green when valid
- Don't show error for empty field
💡 Hint
Use a regex like /^[^\s@]+@[^\s@]+\.[^\s@]+$/ for basic email validation. Track both the value and validity in state.
✅ Solution
import { useState } from 'react';
import { TextInput, View, Text, StyleSheet } from 'react-native';
export default function EmailValidation() {
const [email, setEmail] = useState('');
const [status, setStatus] = useState<'empty' | 'valid' | 'invalid'>('empty');
const validateEmail = (text: string) => {
setEmail(text);
if (text === '') {
setStatus('empty');
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
setStatus(emailRegex.test(text) ? 'valid' : 'invalid');
};
return (
<View style={styles.container}>
<Text style={styles.label}>Email Address</Text>
<TextInput
style={[
styles.input,
status === 'invalid' && styles.inputError,
status === 'valid' && styles.inputValid,
]}
placeholder="you@example.com"
value={email}
onChangeText={validateEmail}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
autoComplete="email"
/>
{status === 'invalid' && (
<Text style={styles.errorText}>✗ Please enter a valid email</Text>
)}
{status === 'valid' && (
<Text style={styles.validText}>✓ Valid email</Text>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 20,
},
label: {
fontSize: 16,
fontWeight: '600',
marginBottom: 8,
color: '#333',
},
input: {
borderWidth: 2,
borderColor: '#ddd',
borderRadius: 8,
padding: 14,
fontSize: 16,
backgroundColor: '#fff',
},
inputError: {
borderColor: '#f44336',
backgroundColor: '#ffebee',
},
inputValid: {
borderColor: '#4CAF50',
backgroundColor: '#e8f5e9',
},
errorText: {
color: '#f44336',
marginTop: 6,
fontSize: 14,
},
validText: {
color: '#4CAF50',
marginTop: 6,
fontSize: 14,
},
});
Exercise 2: Password with Visibility Toggle
Goal: Create a password input with show/hide toggle and strength indicator.
Requirements:
- Secure text entry by default
- Eye icon button to toggle visibility
- Password strength indicator (Weak/Medium/Strong)
- Strength based on: length, has number, has special char
💡 Hint
Calculate strength score: +1 for length ≥ 8, +1 for including number, +1 for special character. 0-1 = Weak, 2 = Medium, 3 = Strong.
✅ Solution
import { useState } from 'react';
import { TextInput, View, Text, Pressable, StyleSheet } from 'react-native';
type Strength = 'weak' | 'medium' | 'strong' | null;
export default function PasswordWithStrength() {
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const getStrength = (pwd: string): Strength => {
if (pwd.length === 0) return null;
let score = 0;
if (pwd.length >= 8) score++;
if (/\d/.test(pwd)) score++;
if (/[!@#$%^&*(),.?":{}|<>]/.test(pwd)) score++;
if (score <= 1) return 'weak';
if (score === 2) return 'medium';
return 'strong';
};
const strength = getStrength(password);
const strengthColors = {
weak: '#f44336',
medium: '#FF9800',
strong: '#4CAF50',
};
return (
<View style={styles.container}>
<Text style={styles.label}>Password</Text>
<View style={styles.inputWrapper}>
<TextInput
style={styles.input}
placeholder="Enter password"
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
autoCapitalize="none"
autoCorrect={false}
/>
<Pressable
onPress={() => setShowPassword(!showPassword)}
style={styles.toggleButton}
hitSlop={10}
>
<Text style={styles.toggleIcon}>
{showPassword ? '🙈' : '👁️'}
</Text>
</Pressable>
</View>
{strength && (
<View style={styles.strengthContainer}>
<View style={styles.strengthBars}>
<View style={[
styles.strengthBar,
{ backgroundColor: strengthColors[strength] }
]} />
<View style={[
styles.strengthBar,
strength !== 'weak' && { backgroundColor: strengthColors[strength] }
]} />
<View style={[
styles.strengthBar,
strength === 'strong' && { backgroundColor: strengthColors[strength] }
]} />
</View>
<Text style={[styles.strengthText, { color: strengthColors[strength] }]}>
{strength.charAt(0).toUpperCase() + strength.slice(1)}
</Text>
</View>
)}
<Text style={styles.hint}>
Use 8+ characters, numbers, and special characters
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 20,
},
label: {
fontSize: 16,
fontWeight: '600',
marginBottom: 8,
color: '#333',
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
backgroundColor: '#fff',
},
input: {
flex: 1,
padding: 14,
fontSize: 16,
},
toggleButton: {
padding: 14,
},
toggleIcon: {
fontSize: 20,
},
strengthContainer: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 10,
gap: 10,
},
strengthBars: {
flexDirection: 'row',
gap: 4,
},
strengthBar: {
width: 40,
height: 4,
borderRadius: 2,
backgroundColor: '#e0e0e0',
},
strengthText: {
fontSize: 12,
fontWeight: '600',
},
hint: {
fontSize: 12,
color: '#666',
marginTop: 8,
},
});
Exercise 3: Multi-Field Form with Focus Flow
Goal: Create a contact form with proper keyboard navigation.
Requirements:
- Fields: Name, Email, Phone, Message (multiline)
- "Next" button navigates to next field
- "Done" on last field dismisses keyboard
- Form submission when all required fields filled
- Character count for message (max 500)
💡 Hint
Create refs for each field. Use returnKeyType="next" and onSubmitEditing to move focus. For multiline, use blurOnSubmit={true} since Return adds newlines.
✅ Solution
import { useRef, useState } from 'react';
import {
TextInput,
View,
Text,
Pressable,
StyleSheet,
Alert,
KeyboardAvoidingView,
ScrollView,
Platform,
} from 'react-native';
export default function ContactForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [message, setMessage] = useState('');
const emailRef = useRef<TextInput>(null);
const phoneRef = useRef<TextInput>(null);
const messageRef = useRef<TextInput>(null);
const maxMessage = 500;
const handleSubmit = () => {
if (!name.trim() || !email.trim() || !message.trim()) {
Alert.alert('Error', 'Please fill in all required fields');
return;
}
Alert.alert('Success', 'Message sent!', [
{ text: 'OK', onPress: () => {
setName('');
setEmail('');
setPhone('');
setMessage('');
}}
]);
};
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.container}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
>
<Text style={styles.title}>Contact Us</Text>
<Text style={styles.label}>Name *</Text>
<TextInput
style={styles.input}
placeholder="Your name"
value={name}
onChangeText={setName}
autoCapitalize="words"
returnKeyType="next"
onSubmitEditing={() => emailRef.current?.focus()}
blurOnSubmit={false}
/>
<Text style={styles.label}>Email *</Text>
<TextInput
ref={emailRef}
style={styles.input}
placeholder="you@example.com"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
returnKeyType="next"
onSubmitEditing={() => phoneRef.current?.focus()}
blurOnSubmit={false}
/>
<Text style={styles.label}>Phone</Text>
<TextInput
ref={phoneRef}
style={styles.input}
placeholder="(optional)"
value={phone}
onChangeText={setPhone}
keyboardType="phone-pad"
returnKeyType="next"
onSubmitEditing={() => messageRef.current?.focus()}
blurOnSubmit={false}
/>
<Text style={styles.label}>Message *</Text>
<TextInput
ref={messageRef}
style={[styles.input, styles.textArea]}
placeholder="How can we help?"
value={message}
onChangeText={(text) => {
if (text.length <= maxMessage) setMessage(text);
}}
multiline
numberOfLines={4}
textAlignVertical="top"
blurOnSubmit={true}
returnKeyType="done"
/>
<Text style={styles.charCount}>
{message.length}/{maxMessage}
</Text>
<Pressable
style={styles.button}
onPress={handleSubmit}
>
<Text style={styles.buttonText}>Send Message</Text>
</Pressable>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
scrollContent: {
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
color: '#333',
},
label: {
fontSize: 14,
fontWeight: '600',
color: '#333',
marginBottom: 6,
marginTop: 12,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 14,
fontSize: 16,
backgroundColor: '#fff',
},
textArea: {
minHeight: 120,
},
charCount: {
textAlign: 'right',
fontSize: 12,
color: '#666',
marginTop: 4,
},
button: {
backgroundColor: '#2196F3',
paddingVertical: 16,
borderRadius: 8,
alignItems: 'center',
marginTop: 24,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
});
Challenge: Search Input with Debounce
🏆 Bonus Challenge
Goal: Create a search input that debounces API calls.
Features:
- Search keyboard type with Search button
- Clear button to reset search
- Debounce: only "search" after user stops typing for 300ms
- Show loading indicator while "searching"
- Display search results (mock data is fine)
✅ Solution
import { useState, useEffect, useRef } from 'react';
import {
TextInput,
View,
Text,
Pressable,
StyleSheet,
FlatList,
ActivityIndicator,
} from 'react-native';
// Mock data
const ALL_ITEMS = [
'Apple', 'Apricot', 'Avocado', 'Banana', 'Blackberry',
'Blueberry', 'Cherry', 'Coconut', 'Date', 'Dragon fruit',
'Fig', 'Grape', 'Grapefruit', 'Guava', 'Kiwi', 'Lemon',
'Lime', 'Mango', 'Melon', 'Orange', 'Papaya', 'Peach',
'Pear', 'Pineapple', 'Plum', 'Pomegranate', 'Raspberry',
'Strawberry', 'Watermelon',
];
// Simulate API delay
const mockSearch = (query: string): Promise<string[]> => {
return new Promise((resolve) => {
setTimeout(() => {
if (!query) {
resolve([]);
return;
}
const results = ALL_ITEMS.filter(item =>
item.toLowerCase().includes(query.toLowerCase())
);
resolve(results);
}, 500);
});
};
export default function SearchWithDebounce() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(false);
const debounceTimer = useRef<NodeJS.Timeout>();
useEffect(() => {
// Clear previous timer
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
// Don't search for empty query
if (!query.trim()) {
setResults([]);
setIsLoading(false);
return;
}
// Set loading immediately for feedback
setIsLoading(true);
// Debounce the search
debounceTimer.current = setTimeout(async () => {
const searchResults = await mockSearch(query);
setResults(searchResults);
setIsLoading(false);
}, 300);
// Cleanup
return () => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
};
}, [query]);
const clearSearch = () => {
setQuery('');
setResults([]);
};
return (
<View style={styles.container}>
<View style={styles.searchContainer}>
<Text style={styles.searchIcon}>🔍</Text>
<TextInput
style={styles.input}
placeholder="Search fruits..."
value={query}
onChangeText={setQuery}
keyboardType="web-search"
returnKeyType="search"
autoCapitalize="none"
autoCorrect={false}
/>
{isLoading ? (
<ActivityIndicator size="small" color="#2196F3" />
) : query.length > 0 ? (
<Pressable onPress={clearSearch} hitSlop={10}>
<Text style={styles.clearButton}>✕</Text>
</Pressable>
) : null}
</View>
{query.length > 0 && !isLoading && (
<Text style={styles.resultCount}>
{results.length} result{results.length !== 1 ? 's' : ''} found
</Text>
)}
<FlatList
data={results}
keyExtractor={(item) => item}
renderItem={({ item }) => (
<Pressable style={styles.resultItem}>
<Text style={styles.resultText}>{item}</Text>
</Pressable>
)}
ListEmptyComponent={
query.length > 0 && !isLoading ? (
<Text style={styles.emptyText}>No results found</Text>
) : null
}
contentContainerStyle={styles.resultsList}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#f5f5f5',
},
searchContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fff',
borderRadius: 12,
paddingHorizontal: 12,
borderWidth: 1,
borderColor: '#e0e0e0',
},
searchIcon: {
fontSize: 18,
marginRight: 8,
},
input: {
flex: 1,
paddingVertical: 14,
fontSize: 16,
},
clearButton: {
fontSize: 18,
color: '#999',
padding: 4,
},
resultCount: {
fontSize: 12,
color: '#666',
marginTop: 12,
marginBottom: 8,
},
resultsList: {
paddingBottom: 20,
},
resultItem: {
backgroundColor: '#fff',
padding: 16,
borderRadius: 8,
marginBottom: 8,
borderWidth: 1,
borderColor: '#e0e0e0',
},
resultText: {
fontSize: 16,
color: '#333',
},
emptyText: {
textAlign: 'center',
color: '#999',
marginTop: 40,
fontSize: 16,
},
});
Summary
🎉 Key Takeaways
- TextInput has no default styling — always add border, padding, background
- Controlled inputs with
valueandonChangeTextare recommended - keyboardType shows the right keyboard: email-address, phone-pad, numeric, etc.
- secureTextEntry hides password characters
- multiline={true} creates text areas; use
textAlignVertical="top"on Android - Focus management with refs:
inputRef.current?.focus() - returnKeyType changes the keyboard button: next, done, search, go, send
- Keyboard dismissal: Keyboard.dismiss(), TouchableWithoutFeedback, or ScrollView modes
- Platform differences: Android has underline by default, iOS doesn't
Quick Reference
// Basic controlled input
<TextInput
value={text}
onChangeText={setText}
placeholder="Enter text..."
style={styles.input}
/>
// Email input
<TextInput
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
textContentType="emailAddress"
/>
// Password input
<TextInput
secureTextEntry
textContentType="password"
autoComplete="password"
/>
// Multiline
<TextInput
multiline
numberOfLines={4}
textAlignVertical="top"
/>
// Focus navigation
<TextInput
returnKeyType="next"
onSubmitEditing={() => nextRef.current?.focus()}
blurOnSubmit={false}
/>
// Keyboard dismissal
import { Keyboard } from 'react-native';
Keyboard.dismiss();
// ScrollView with dismissal
<ScrollView
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
/>
Common Patterns Cheat Sheet
Input with Label
<View>
<Text style={styles.label}>
Email
</Text>
<TextInput ... />
</View>
Show/Hide Password
secureTextEntry={!show}
// + toggle button
Character Limit
maxLength={150}
// or in onChangeText
Error State
style={[
styles.input,
error && styles.error
]}
🚀 What's Next?
Now that you can capture text input, the final lesson in this module covers Switch, ActivityIndicator, and StatusBar — three utility components for toggles, loading states, and controlling the device status bar.
⌨️ Input Mastered!
You now have complete control over text input in your apps. From simple text fields to complex multi-field forms with validation, keyboard management, and focus flow — your forms will feel polished and professional!