Text field

Free · MITReact Native · Expo

A text field whose label glides up as you focus it, with helper and error text that slide in, a shake and haptic on error, clear and reveal buttons, a character counter and a multiline mode that grows with its text.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/text-field.json

Then run npx expo install --fix so react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/text-field.

Usage

import { useState } from "react";
import { TextField } from "@/components/goodui-native/text-field/text-field";

export function EmailField() {
  const [email, setEmail] = useState("");
  const [error, setError] = useState<string>();
  return (
    <TextField
      label="Email"
      placeholder="name@example.com"
      value={email}
      onChangeText={(t) => {
        setEmail(t);
        setError(undefined);
      }}
      onBlur={() => setError(email.includes("@") ? undefined : "Enter a valid email address")}
      error={error}
      helperText="We'll send the receipt here"
      keyboardType="email-address"
      autoComplete="email"
      autoCapitalize="none"
    />
  );
}

When to use

Use it for
  • Form fields: name, email, address, card holder, notes.
  • Passwords and secrets (secureTextEntry adds the reveal toggle).
  • Free text with a limit, such as a bio or a review (showCount with maxLength).
  • Notes and messages that should grow while typing (multiline with minRows and maxRows).
Reach for something else
  • Search: use search-bar, which has Cancel, scopes and debouncing.
  • Verification codes: use otp-input.
  • Numbers changed by small steps (guests, quantity): use quantity-stepper.
  • Picking from a fixed list: use segmented-control or a radio group from checkbox.

Examples

Password with reveal toggle

<TextField label="Password" secureTextEntry textContentType="password" autoComplete="password" />

Growing note with a limit

<TextField label="Note for the driver" multiline minRows={2} maxRows={6} maxLength={200} showCount />

Amount with a unit and an icon

<TextField label="Amount" keyboardType="decimal-pad" leading={<CardIcon />} trailing={<Text tone="secondary">EUR</Text>} />

Focus the next field

const next = useRef<ComponentRef<typeof TextInput>>(null);

<TextField label="First name" returnKeyType="next" onSubmitEditing={() => next.current?.focus()} />
<TextField label="Last name" inputRef={next} />

API reference

PropTypeDefault
label
Floating label. It rests inside the field like a placeholder and glides up to the top edge when the field is focused or has text. Sentence case.
string—
value
Text value (controlled). Pair it with `onChangeText`. Omit it to let the field keep its own text.
string—
defaultValue
Initial text when uncontrolled.
string""
onChangeText
Called on every change, including the clear button (with "").
(text: string) => void—
placeholder
Example input such as "name@example.com". With a label it shows only once the label has floated and the field is still empty; without a label it is the placeholder.
string—
helperText
Supporting text under the field. An error message replaces it while present.
string—
error
Error state. A string is shown under the field in place of the helper text. When an error appears (or changes) the field shakes once, plays the error haptic and is announced. `true` marks the field red without a message.
string | boolean—
leading
Icon before the text, about 20 pt. Color it with `textSecondary`.
ReactNode—
trailing
Icon or short text after the input, for example a unit ("kg") or a badge.
ReactNode—
clearable
Shows a clear button while the field is focused and has text. Default `true` for single-line fields that are not secure.
boolean—
secureTextEntry
Hides the text and adds an eye button that reveals it.
booleanfalse
maxLength
Maximum number of characters.
number—
showCount
Shows a "12/80" counter under the field. Needs `maxLength`. Reaching the limit plays a warning haptic.
booleanfalse
multiline
Multiline field that grows with its text between `minRows` and `maxRows`, then scrolls.
booleanfalse
minRows
Rows a multiline field starts with. Default 3.
number3
maxRows
Rows a multiline field grows to before it scrolls. Default 8.
number8
variant
`filled` is a quiet muted surface; `outline` draws a hairline border for dense forms on a plain surface.
"filled" | "outline""filled"
size
Heights 48 (`sm`) and 56 pt (`md`).
"sm" | "md""md"
disabled
Dims the field and makes it read-only.
booleanfalse
haptic
`false` silences this field's haptics (error, limit reached, reveal toggle, clear).
booleantrue
onFocus
Called when the input gains focus.
() => void—
onBlur
Called when the input loses focus.
() => void—
inputRef
Ref to the underlying TextInput, for example to focus the next field from `onSubmitEditing`.
Ref<ComponentRef<typeof TextInput>>—
accessibilityLabel
Screen reader label. Defaults to `label`, then `placeholder`.
string—
accessibilityHint
Screen reader hint. The error or helper text is used when omitted.
string—
labels
Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide.
Partial<TextFieldLabels>—
style
Style of the outer wrapper (field plus message line).
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—

Motion and haptics

  • Label: rests at body size inside the field and glides up and shrinks to caption size on the morph spring when focused or filled; its color follows focus (accent) and error (danger).
  • Focus draws a 2 pt accent ring; an error turns the ring and label red and tints the filled surface.
  • An error that appears or changes shakes the field once.
  • Helper and error text slide down into place; the message line's height springs, so content below glides instead of jumping.
  • The clear button scales in and its slot opens on the snappy spring; the reveal toggle's slash draws across or retracts.
  • Multiline: the field's height springs to fit its text between minRows and maxRows, then scrolls.
  • Reduced motion: the label moves with a short fade-speed timing instead of a spring, no shake, no scaling, heights change instantly; colors still change.
  • Haptic: error when an error appears or changes (never on mount)
  • Haptic: warning once when the text reaches maxLength
  • Haptic: select on the reveal toggle
  • Haptic: tap on clear
  • Label, ring, colors, shake and heights animate on the UI thread through Reanimated shared values.
  • Typing re-renders the field once per keystroke like any controlled TextInput; nothing animates per keystroke except the multiline height when the line count changes.

Accessibility

  • The TextInput is the accessible element. Its label is accessibilityLabel, then label, then placeholder; the floating label itself is hidden from screen readers.
  • The hint is accessibilityHint, else the error or helper text, so the reason for an error is read with the field.
  • A new error message is announced with AccessibilityInfo.announceForAccessibility.
  • Clear is a button labelled 'Clear text' and is hidden from screen readers while invisible. The reveal toggle is a button labelled 'Show password' or 'Hide password'.
  • Icon buttons have a 12 pt (clear) or 10 pt (reveal) hitSlop so they reach a 44 pt target.
  • On the web a secure field autofills as current-password; Android's 'password' and 'password-new' map to current-password and new-password there.

Notes for AI

  • Use label for the field name and placeholder for an example value. Do not pass both as the same text.
  • Pass error as a string to show a message, or true for a red state without one; set it back to undefined to clear it. The field shakes and buzzes by itself, so do not add your own shake or haptic.
  • It works controlled (value + onChangeText) or uncontrolled (defaultValue).
  • Use inputRef, not ref, to reach the TextInput (for focus() from returnKeyType='next').
  • Inside a ScrollView set keyboardShouldPersistTaps='handled' so the clear and reveal buttons work while the keyboard is up.
  • clearable defaults to true for single-line, non-secure fields; secureTextEntry fields get the reveal toggle instead.