Skip to main content

⌨️ 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.

Common Keyboard Types default Q W E R T Y U I O P A S D F G H J K L Z X C V B N M space email-address Q W E R T Y U I O P A S D F G H J K L Z X C V B N M @ . .com numeric 1 2 3 4 5 6 7 8 9 - 0 . phone-pad 1 2 3 4 5 6 7 8 9 * 0 # number-pad 1 2 3 4 5 6 7 8 9 0 ⌫ decimal-pad 1 2 3 4 5 6 7 8 9 . 0 ⌫ url Q W E R T Y U I O P A S D F G H J K L Z X C V B N M / . Go web-search Q W E R T Y U I O P A S D F G H J K L Z X C V B N M space Search

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-address with autoCapitalize="none"
  • Use decimal-pad for currency/prices, number-pad for counts
  • phone-pad still 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 (default) Hello world multiline={false} Return key submits Multiline Hello world This is a longer piece of text that spans multiple lines multiline={true} Return key adds new line

Single-line inputs vs multiline text areas

Multiline Tips

⚠️ Multiline Gotchas

  • numberOfLines only sets initial height on Android — use minHeight for 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 Default (no styling) Placeholder With border styling Placeholder • No underline by default • Padding must be added Android Default (no styling) Placeholder With border styling Placeholder • Has underline by default • Some default padding

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 value and onChangeText are 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!