TextInput

How TextInput works as a controlled input, and the props that decide which keyboard the reader gets.

Syntax<TextInput value={text} onChangeText={(newText: string) => void} />

TextInput is the only text entry component in React Native. There is no <input> and no <textarea>. One component covers every case, and props decide how it behaves.

Controlled input

You hold the text in state and pass it down as value. When the user types, onChangeText fires with the complete new string, and you write it back to state. The input shows whatever is in state, so state is the truth.

Note the argument. onChangeText receives a plain string, not an event. There is no event.target.value here. There is an onChange prop that gives you { nativeEvent: { text, eventCount, target } }, but onChangeText is what you normally want.

Shaping the keyboard

keyboardType picks the on-screen keyboard: 'default', 'email-address', 'numeric', 'number-pad', 'decimal-pad', 'phone-pad', 'url'. autoCapitalize defaults to 'sentences', which is wrong for emails and usernames, so set 'none'. secureTextEntry masks the characters. inputMode is a newer cross-platform alternative to keyboardType with web-style values such as 'email' and 'decimal'.

Placeholder is not a label

placeholder text disappears the moment the field has content, and screen readers treat it as a hint rather than a name. There is no label prop. Render a Text above the field instead.

Shorthand and longhand

onChangeText={setEmail} is the same as onChangeText={(text) => setEmail(text)}. secureTextEntry on its own means secureTextEntry={true}.

Example

Example · tsx
import { useState } from "react";
import { StyleSheet, Text, TextInput, View } from "react-native";

// On screen: a bold "Email" label, a bordered field below it,
// and a live character count underneath that updates as you type.
export default function EmailField() {
  const [email, setEmail] = useState("");

  return (
    <View style={styles.container}>
      {/* TextInput has no label prop, so the label is a Text of its own */}
      <Text style={styles.label}>Email</Text>
      <TextInput
        style={styles.input}
        value={email}            // state drives what the field shows
        onChangeText={setEmail}  // fires with the whole new string, not an event
        placeholder="[email protected]"
        placeholderTextColor="#8a8a8e"
        keyboardType="email-address"
        autoCapitalize="none"    // default is "sentences", wrong for an email
        autoCorrect={false}
      />
      <Text style={styles.hint}>{email.length} characters</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { padding: 16, gap: 8 },
  label: { fontSize: 13, fontWeight: "600", color: "#3c3c43" },
  input: {
    borderWidth: 1,
    borderColor: "#c6c6c8",
    borderRadius: 8,
    paddingHorizontal: 12,
    paddingVertical: 10,
    fontSize: 16,
  },
  hint: { fontSize: 12, color: "#8a8a8e" },
});

When to use it

  • Collecting an email and password on a sign-in screen with the right keyboard for each.
  • Letting the user edit a profile name where the current value is loaded into state first.
  • Accepting a quantity or price with a numeric keypad and your own filtering.
  • Capturing a free-text note or review in a multiline box.

More examples

Password field with a reveal toggle

The field and the button sit side by side; tapping "Show" swaps the dots for the real characters. Note flexDirection: "row" is set explicitly, because the default in React Native is column, not row. Reach for this whenever a password rule is strict enough that users need to check what they typed.

Example · tsx
import { useState } from "react";
import { Pressable, StyleSheet, Text, TextInput, View } from "react-native";

export default function PasswordField() {
  const [password, setPassword] = useState("");
  const [revealed, setRevealed] = useState(false);

  return (
    <View style={styles.row}>
      <TextInput
        style={styles.input}
        value={password}
        onChangeText={setPassword}
        placeholder="Password"
        secureTextEntry={!revealed}
        autoCapitalize="none"
        autoCorrect={false}
        textContentType="password" // iOS: lets the keychain offer a saved password
      />
      <Pressable onPress={() => setRevealed(!revealed)} hitSlop={8}>
        <Text style={styles.toggle}>{revealed ? "Hide" : "Show"}</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  row: { flexDirection: "row", alignItems: "center", gap: 12, padding: 16 },
  input: { flex: 1, borderWidth: 1, borderColor: "#c6c6c8", borderRadius: 8, padding: 10 },
  toggle: { color: "#0a84ff", fontWeight: "600" },
});

Digits only, enforced in state

The field accepts at most six digits and silently drops anything else, including pasted text. This is the pattern for codes, PINs and card numbers, where number-pad alone is not enough because paste bypasses the keyboard entirely.

Example · tsx
import { useState } from "react";
import { StyleSheet, TextInput } from "react-native";

// Pure helper, so it is easy to test on its own.
export function digitsOnly(text: string): string {
  return text.replace(/[^0-9]/g, "");
}
// Output: digitsOnly("12a3-4") === "1234"

export default function PinField() {
  const [pin, setPin] = useState("");

  return (
    <TextInput
      style={styles.input}
      value={pin}
      // Filter on the way into state; the field re-renders from state,
      // so a pasted letter never survives a single frame.
      onChangeText={(text) => setPin(digitsOnly(text).slice(0, 6))}
      keyboardType="number-pad"
      maxLength={6}
      placeholder="000000"
    />
  );
}

const styles = StyleSheet.create({
  input: { margin: 16, padding: 12, borderWidth: 1, borderColor: "#c6c6c8", borderRadius: 8, letterSpacing: 4 },
});

Multiline notes field

A four-line box whose text begins at the top left on both platforms. numberOfLines caps the field at four lines, and since the New Architecture became the default it applies on iOS too, not only on Android. textAlignVertical is the one Android-only prop here, and leaving it out is the usual reason a multiline field looks right on one platform and centred on the other. minHeight in the style is what gives the box a comfortable starting height, because numberOfLines sets a maximum rather than an initial size.

Example · tsx
import { useState } from "react";
import { StyleSheet, TextInput } from "react-native";

export default function NotesField() {
  const [notes, setNotes] = useState("");

  return (
    <TextInput
      style={styles.input}
      value={notes}
      onChangeText={setNotes}
      multiline                 // same as multiline={true}
      numberOfLines={4}         // caps the field at four lines; on iOS this needs the New Architecture
      textAlignVertical="top"   // Android only: without it, text starts centred
      placeholder="What happened?"
    />
  );
}

const styles = StyleSheet.create({
  input: {
    margin: 16,
    minHeight: 96,     // a comfortable starting height; numberOfLines sets a maximum, not an initial size
    padding: 12,
    borderWidth: 1,
    borderColor: "#c6c6c8",
    borderRadius: 8,
  },
});

Discussion

  • Be the first to comment on this lesson.