World’s largest virtual agentic engineering & quality conference
Learn React Native TextInput: every prop, controlled vs uncontrolled patterns, styling, focus management, validation, accessibility, and how to test it.

Salman Khan
Author

Shivam Singh
Reviewer
Last Updated on: August 10, 2026
React Native's GitHub repository carries 126,000+ stars, and it's the framework behind Meta's own apps, Microsoft Teams and Outlook, Discord, Shopify, Coinbase, and Tesla. Nearly every one of those apps shares the same interaction-layer building block: a TextInput field handling a login, a search query, or a chat message.
Most TextInput tutorials stop at "here's how to read a value." This guide covers the full surface: the complete prop reference, controlled vs uncontrolled patterns, styling (including the platform quirks that break a field's look on only one OS), focus and ref management, validation, accessibility, and - the part almost every other guide skips - how to actually test a TextInput on Appium and Detox.
Overview
TextInput is React Native's core component for capturing typed user input, rendering a native UITextField on iOS and an EditText on Android behind one cross-platform API. Getting it right means handling controlled state, platform-specific styling, focus management, validation, accessibility, and - the part most guides skip - testing it reliably with Appium or Detox.
Core Concepts in This Guide
TextInput is the component React Native provides for accepting typed text from the user. Under the hood, it wraps a native UITextField or UITextView on iOS and a native EditText on Android, so the keyboard, text rendering, and cursor behavior all come from the platform itself rather than a JavaScript re-implementation.
That native wrapping is exactly why TextInput behaves slightly differently on iOS and Android for the same code - a theme that runs through styling, keyboard handling, and testing throughout this guide. The full, current prop list lives on the official React Native TextInput documentation; the next section covers the props developers reach for most often.
These are the props that cover the vast majority of real-world TextInput usage, from a basic field to a fully validated, accessible form input:
| Prop | Type | What It Does |
|---|---|---|
| value | string | Controlled component value - the native field is forced to match this prop |
| defaultValue | string | Initial value for an uncontrolled input; changes as the user types without needing state |
| onChangeText | function | Fires on every keystroke, passing the new text as a plain string |
| onChange | function | Fires on every keystroke, passing a nativeEvent object instead of a plain string |
| placeholder | string | Hint text shown before the user enters a value |
| keyboardType | enum, default 'default' | Selects the keyboard variant: numeric, email-address, phone-pad, url, decimal-pad, and more (some values are iOS- or Android-only) |
| secureTextEntry | bool, default false | Obscures typed characters for passwords; does not work when multiline is true |
| multiline | bool, default false | Allows multiple lines; aligns text to the top on iOS and centers it on Android by default |
| maxLength | number | Hard character limit, enforced in native code to avoid input flicker |
| editable | bool, default true | Set to false to render the field read-only |
| autoFocus | bool, default false | Focuses the field automatically when it mounts |
| autoCapitalize | enum, default 'sentences' | Controls automatic capitalization: characters, words, sentences, or none |
| returnKeyType | enum | Changes the keyboard's return key label (done, go, next, search, send, and more) |
| onSubmitEditing | function | Fires when the return key is pressed; doesn't fire on iOS when keyboardType is phone-pad |
| onFocus / onBlur | function | Fire when the field gains or loses focus - the hook point for focus-based styling |
| testID | string | The identifier automation frameworks use to find the field; maps to resource-id on Android and accessibility-id on iOS |
A controlled TextInput ties its value prop to component state, so React owns the source of truth and re-renders the field on every keystroke. This is the pattern to use whenever you need to validate, transform, or submit the value:
import { useState } from 'react';
import { TextInput } from 'react-native';
function ControlledEmailInput() {
const [email, setEmail] = useState('');
return (
<TextInput
value={email}
onChangeText={setEmail}
placeholder="Email address"
keyboardType="email-address"
autoCapitalize="none"
testID="email-input"
/>
);
}
An uncontrolled TextInput uses defaultValue instead of value, so the native field manages its own text internally and React never re-renders on keystrokes. It's a reasonable choice for a field you read only once, like on a form-submit button press via a ref, but it can't drive validation-as-you-type or a character counter, since your component never sees the intermediate values.
Note: Testing controlled and uncontrolled TextInput behavior on real device keyboards catches autofill and rendering issues an emulator can't reproduce. Try TestMu AI Now!
TextInput accepts a standard style prop, but a few platform quirks catch developers coming from web CSS off guard. Android renders a default underline unless you explicitly remove it, and there's no native :focus pseudo-class - focus-based styling has to be built with state:
import { useState } from 'react';
import { TextInput, StyleSheet } from 'react-native';
function StyledInput() {
const [isFocused, setIsFocused] = useState(false);
return (
<TextInput
style={[styles.input, isFocused && styles.inputFocused]}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
placeholder="Username"
placeholderTextColor="#9CA3AF"
underlineColorAndroid="transparent"
/>
);
}
const styles = StyleSheet.create({
input: {
borderWidth: 1,
borderColor: '#D1D5DB',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 16,
},
inputFocused: {
borderColor: '#2563EB',
borderWidth: 2,
},
});
Three styling mistakes account for most TextInput visual bugs:
These platform differences are also why a field that looks correct in a simulator during development can still render wrong on a real device - simulators don't always reproduce native rendering quirks with full fidelity. Validating the actual visual output on real hardware is what TestMu AI's Real Device Cloud is built for: 10,000+ real Android and iOS devices, so a styling fix gets verified against the same rendering engine your users' phones actually run.
A ref attached to TextInput exposes four imperative methods: .focus(), .blur(), .clear(), and .isFocused(). The most common real-world use is auto-advancing focus from one field to the next, a pattern seen in OTP inputs, address forms, and card-number entry:
import { useRef } from 'react';
import { TextInput, View } from 'react-native';
function NameForm() {
const lastNameRef = useRef(null);
return (
<View>
<TextInput
placeholder="First name"
returnKeyType="next"
onSubmitEditing={() => lastNameRef.current.focus()}
blurOnSubmit={false}
/>
<TextInput
ref={lastNameRef}
placeholder="Last name"
returnKeyType="done"
/>
</View>
);
}
blurOnSubmit={false} on the first field stops the keyboard from dismissing between fields, so the chain feels continuous instead of flickering the keyboard closed and open again.
The manual validation pattern runs a check inside onChangeText or onBlur and stores the result in state that drives an error message and an error style:
import { useState } from 'react';
import { TextInput, Text as RNText, View } from 'react-native';
function EmailField() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const validate = (text) => {
setEmail(text);
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text);
setError(text.length > 0 && !isValid ? 'Enter a valid email address' : '');
};
return (
<View>
<TextInput
value={email}
onChangeText={validate}
style={error ? { borderColor: 'red' } : { borderColor: '#D1D5DB' }}
keyboardType="email-address"
autoCapitalize="none"
/>
{error ? <RNText style={{ color: 'red' }}>{error}</RNText> : null}
</View>
);
}
For forms with more than two or three fields, this pattern gets repetitive fast. React Hook Form and Formik both support React Native and wrap the same value/onChangeText/error-state cycle behind a Controller component, centralizing validation rules and error messages instead of hand-rolling them per field. Field-level validation like this only confirms the individual input works - see React End-to-End Testing Tutorial for validating the full form submission flow.
A TextInput with no label text next to it - a common pattern for search bars and minimalist forms - is invisible to a screen reader user unless it carries accessibility props explicitly:
These props only cover the field's own labeling. Verifying that VoiceOver and TalkBack actually navigate the field correctly still requires manually toggling the screen reader on a real device - it's a native OS behavior that a simulator can approximate but not fully guarantee, the same real-hardware-versus-simulator gap covered in the styling section above.
Since React Native 0.64, a TextInput's testID prop exposes automatically as the element's resource-id on Android and its accessibility-id on iOS - no separate accessibility workaround needed. That's the identifier both major mobile test frameworks use to find the field.
Appium locates the field through the standard W3C WebDriver protocol, using the testID as the accessibility id or resource-id selector, then sends keys to it like any native input:
const { remote } = require('webdriverio');
const capabilities = {
platformName: 'Android',
'appium:deviceName': 'Samsung Galaxy S24',
'appium:platformVersion': '14',
'appium:automationName': 'UiAutomator2',
'appium:app': 'lt://APP_URL_OR_ID',
'lt:options': {
username: process.env.LT_USERNAME,
accessKey: process.env.LT_ACCESS_KEY,
isRealMobile: true,
build: 'RN TextInput Suite',
},
};
const driver = await remote({
protocol: 'https',
hostname: 'mobile-hub.lambdatest.com',
path: '/wd/hub',
capabilities,
});
const emailField = await driver.$('~email-input');
await emailField.setValue('user@example.com');
The isRealMobile: true capability is what routes that session to physical hardware instead of an emulator - without it, Appium sessions default to an emulator or simulator. For a deeper walkthrough of setting testIDs up correctly for Appium in the first place, see Best Practices for React Native Development to Improve Appium Test Automation.
Detox, the React Native-specific alternative, uses the same testID through its by.id matcher, with two distinct text-entry actions:
describe('Email input', () => {
it('should accept a typed email address', async () => {
await element(by.id('email-input')).typeText('user@example.com');
await expect(element(by.id('email-input'))).toHaveText('user@example.com');
});
it('should replace existing text instantly', async () => {
await element(by.id('email-input')).replaceText('new@example.com');
});
});
typeText simulates the system's real keyboard input character by character, which is slower but exercises the same onChangeText callback path a real user triggers. replaceText sets the value directly and skips the keyboard simulation entirely, which is faster but can leave text-input callbacks in an inconsistent state - reserve it for setup steps where the field's own behavior isn't what's under test.
Real devices surface TextInput failures an emulator won't: soft-keyboard animation timing that races a test's next action, password-manager autofill overlays covering the field before a tap lands, and Android permission dialogs stealing focus mid-flow. TestMu AI's App Automation addresses the timing class of these directly - its SmartWait capability proactively waits for an element to be interactable before attempting an action, instead of failing on a fixed timeout. Combined with App Automation's native support for Appium, Espresso, XCUITest, and Detox across the same 10,000+ real-device fleet, both examples above run unmodified against real hardware.
| Symptom | Cause | Fix |
|---|---|---|
| Field won't accept new characters after a certain point | value is bound to state that isn't being updated by onChangeText, so the field keeps getting reset to the stale value | Confirm onChangeText calls the state setter directly, not a debounced or conditional wrapper around it |
| Password field renders as plain, unmasked text | multiline is set to true alongside secureTextEntry, a documented incompatibility | Keep password fields single-line; there's no supported multiline masked input |
| Keyboard covers the active field | The screen doesn't shift to accommodate the keyboard, which is layout behavior, not a TextInput bug | Wrap the form in KeyboardAvoidingView with an explicit behavior prop per platform |
| Return key does nothing on a phone-number field (iOS) | keyboardType="phone-pad" has no return key on iOS, so onSubmitEditing never fires | Add a visible submit button instead of relying on the keyboard's return key |
| Multiline field text looks vertically off-center on Android only | Android centers multiline text vertically by default; iOS already aligns to the top | Add textAlignVertical="top" to match iOS's default behavior on both platforms |
TextInput looks simple until a real form needs controlled state, platform-consistent styling, focus chaining, validation, accessibility, and reliable test coverage all at once. Getting the props right is the easy half; verifying that the field actually behaves the same way on a real Samsung Galaxy and a real iPhone as it does in a simulator is where most teams stop short.
Run the Appium or Detox examples from this guide against TestMu AI's Real Device Cloud to catch keyboard, autofill, and rendering issues before they reach production, using the Appium getting-started documentation to wire up your existing suite. For testing the rest of the app beyond individual fields, see How to Perform React Native Testing.
Author
Salman is a Test Automation Evangelist and Community Contributor at TestMu AI, with over 6 years of hands-on experience in software testing and automation. He has completed his Master of Technology in Computer Science and Engineering, demonstrating strong technical expertise in software development, testing, AI agents and LLMs. He is certified in KaneAI, Automation Testing, Selenium, Cypress, Playwright, and Appium, with deep experience in CI/CD pipelines, cross-browser testing, AI in testing, and mobile automation. Salman works closely with engineering teams to convert complex testing concepts into actionable, developer-first content. Salman has authored 120+ technical tutorials, guides, and documentation on test automation, web development, and related domains, making him a strong voice in the QA and testing community.
Reviewer
Shivam Singh is a Lead Member of Technical Staff at TestMu AI (formerly LambdaTest), architecting the Real Device Cloud that runs automated app and web tests on real Android and iOS devices. He designed the architecture for real-device app and web automation and wrote the microservices from scratch in Golang, including the XCUITest and Espresso execution layers for iOS and Android. His platform reached peak parallel concurrency of 150+ for app automation while handling roughly 500,000 tests a month, and he leads the team that keeps the automation grid running. He brings over eight years of engineering experience and holds a B.Tech in Computer Science.
Did you find this page helpful?
More Related Blogs
TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance