This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.
TextInput
一个由原生 SwiftUI 和 Jetpack Compose 组件支持的文本输入,具有与 React Native 兼容的 API。
一个文本输入,在 Android 上从 @expo/ui/jetpack-compose 路由到 TextField,在 iOS 上从 @expo/ui/swift-ui 路由到 TextField,以及在 Web 上通过 React Native 的 TextInput 路由。
🌐 A text input that routes to TextField from @expo/ui/jetpack-compose on Android, TextField from @expo/ui/swift-ui on iOS, and React Native's TextInput on web.
该 API 镜像了 React Native 的 TextInput,有两个变化:value 和 selection 是可观察的状态对象(使用 useNativeState 创建),并且 onChangeText 可以是一个 worklet,用于在 UI 线程上同步更新状态。
🌐 The API mirrors React Native's TextInput, with two changes: value and selection are observable state objects (created with useNativeState), and onChangeText can be a worklet for synchronously updating the state on the UI thread.
安装
🌐 Installation
- npx expo install @expo/uiIf you are installing this in an existing React Native app, make sure to install expo in your project.
用法
🌐 Usage
失控的
🌐 Uncontrolled
省略 value,字段会在内部管理自己的文本。使用 onChangeText 来观察编辑,并使用 ref 执行诸如 focus、blur 和 clear 的命令操作。
🌐 Omit value and the field manages its own text internally. Use onChangeText to observe edits, and use the ref for imperative actions like focus, blur, and clear.
import { Button, Column, Host, TextInput, type TextInputRef } from '@expo/ui'; import { useRef } from 'react'; export default function UncontrolledTextInputExample() { const inputRef = useRef<TextInputRef>(null); return ( <Host matchContents={{ vertical: true }}> <Column spacing={8}> <TextInput ref={inputRef} defaultValue="hello" placeholder="Type here" onChangeText={value => console.log(value)} /> <Button label="Clear" onPress={() => inputRef.current?.clear()} /> </Column> </Host> ); }
受控
🌐 Controlled
传递 value 以从 useNativeState 可观察对象驱动该字段。下面的示例在你输入时将 Hello 替换为 World。
🌐 Pass value to drive the field from a useNativeState observable. The example below replaces Hello with World as you type.
import { Host, TextInput, useNativeState } from '@expo/ui'; import { useEffectEvent } from 'react'; export default function ControlledTextInputExample() { const text = useNativeState(''); const handleChangeText = useEffectEvent((value: string) => { 'worklet'; text.value = value === 'Hello' ? 'World' : value; }); return ( <Host matchContents={{ vertical: true }}> <TextInput value={text} placeholder="Type here" onChangeText={handleChangeText} /> </Host> ); }
工作单元掩码
🌐 Worklet masking
将 'worklet' 指令添加到 onChangeText 以在 UI 线程上同步更新状态。对 value 的写入可以直接生效,而无需经过可能导致光标闪烁的 JS 线程往返。
🌐 Add the 'worklet' directive to onChangeText for synchronously updating the state on the UI thread. Writes to value land without the JS-thread round-trip that can cause cursor flicker.
注意: Worklets 需要安装
react-native-worklets。
import { Host, TextInput, useNativeState } from '@expo/ui'; import { useEffectEvent } from 'react'; function formatPhone(input: string) { 'worklet'; const digits = input.replace(/\D/g, '').slice(0, 10); if (digits.length <= 3) return digits; if (digits.length <= 6) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`; return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`; } export default function PhoneMaskExample() { const phone = useNativeState(''); const selection = useNativeState({ start: 0, end: 0 }); const handleChangeText = useEffectEvent((value: string) => { 'worklet'; const formatted = formatPhone(value); if (formatted !== value) { phone.value = formatted; // Snaps to end for demo. Real masks need smarter cursor handling. selection.value = { start: formatted.length, end: formatted.length }; } }); return ( <Host matchContents={{ vertical: true }}> <TextInput value={phone} selection={selection} keyboardType="phone-pad" placeholder="(555) 123-4567" onChangeText={handleChangeText} /> </Host> ); }
不受支持的 React Native 属性
🌐 Unsupported React Native props
一些 React Native 的 TextInput 属性不受支持,因为 Compose 的 TextField 或 SwiftUI 的 TextField 没有提供等效属性,或者因为该属性被其他机制取代。有关支持的属性,请参见下方的 API 部分。如果缺失的属性妨碍了你的使用场景,请 提交问题,以便优先处理。
🌐 Some React Native TextInput props are not supported, because Compose's TextField or SwiftUI's TextField does not expose an equivalent, or because the prop is replaced by a different mechanism. See the API section below for the supported props. If a missing prop blocks your use case, open an issue so it can be prioritized.
应用接口
🌐 API
import { TextInput, useNativeState } from '@expo/ui';
Component
Type: React.Element<TextInputProps>
string • Default: 'sentences'Controls automatic capitalization of input.
Acceptable values are: 'none' | 'words' | 'sentences' | 'characters'
AutoCompleteAutofill hint. iOS maps to textContentType; Android maps to Compose's
Modifier.semantics { contentType = ... }.
boolean • Default: trueIf false, disables autocorrect / spellcheck suggestions.
booleanIf true, the cursor is hidden.
On iOS, this is implemented via tint('transparent'), which also makes
the selection highlight invisible. If you set both caretHidden and
selectionColor, the caret-hide wins on iOS.
stringInitial text shown when the input mounts and value is not provided.
Ignored once the user starts typing or if value is set.
boolean • Default: trueIf false, the input cannot be edited. Selection is still allowed so the user can copy text out of the field.
EnterKeyHintHTML-style hint for the keyboard return key. Maps to returnKeyType.
When both are set, returnKeyType wins.
InputModeHTML-style hint for the keyboard variant. Maps to keyboardType. When
both are set, keyboardType wins.
KeyboardTypeOptions • Default: 'default'Determines which keyboard variant is shown.
Lacking native support:
- iOS:
'visible-password'falls back to the default keyboard. - Android: iOS-specific values (
'ascii-capable','numbers-and-punctuation','name-phone-pad','twitter','web-search') fall back to the text keyboard.
ModifierConfig[]Platform-specific modifier escape hatch. Pass an array of modifier configs
from @expo/ui/swift-ui/modifiers or @expo/ui/jetpack-compose/modifiers.
Modifiers from the wrong platform are ignored at runtime.
boolean • Default: falseIf true, the field accepts multiple lines of input and grows vertically as the user types.
numberNumber of lines the field reserves when multiline is true. Forces a
fixed visible height of that many lines.
Lacking native support:
- iOS: requires iOS 16+; below that, the field grows naturally.
(text: string) => voidCalled every time the text value changes. Receives the new string.
(size: {
height: number,
width: number
}) => voidCalled when the rendered size of the input changes. Sizes in points/dp.
Unlike RN's onContentSizeChange, this dispatches the view's outer
geometry, including any padding/border applied via style or modifiers.
If you use this for autogrow, account for that.
(selection: {
end: number,
start: number
}) => voidCalled when the text selection range changes.
(text: string) => voidCalled when the user taps the keyboard return key. Receives the current text in the input.
boolean • Default: falseAlias for editable={false}. When both are set, editable wins.
ReturnKeyTypeOptionsDetermines the label of the keyboard return key.
Lacking native support:
- iOS:
'emergency-call'falls back to the default Return key. - Android:
'join','route','emergency-call'fall back to the default action.
numberHTML-style alias for numberOfLines. When both are set, numberOfLines wins.
boolean • Default: falseIf true, the input obscures its text — used for password fields.
- iOS: backed by SwiftUI's
SecureField. The following props are no-ops in this mode:selection,selectTextOnFocus,onSelectionChange,multiline,numberOfLines. - Android: backed by Compose's
PasswordVisualTransformation.
ObservableState<{
end: number,
start: number
}>Observable state the field writes the current selection to.
Create with useNativeState({ start: 0, end: 0 }).
Use ref.setSelection(start, end) to set selection programmatically.
ColorValueColor of the selected text highlight. On iOS this also tints the cursor
(UIKit's tintColor covers both); pass cursorColor only if you want
different cursor color on Android.
ColorValueColor of the selection drag handles.
boolean • Default: falseIf true, all text is selected when the field gains focus. Implemented
via setSelection(0, length) on focus, so if you also pass selection,
its value is overwritten on every focus.
Pick<ViewStyle, 'padding' | 'paddingHorizontal' | 'paddingVertical' | 'paddingTop' | 'paddingBottom' | 'paddingLeft' | 'paddingRight' | 'backgroundColor' | 'borderRadius' | 'borderWidth' | 'borderColor' | 'opacity' | 'width' | 'height'>Box-level style — sizing, padding, background, border, opacity.
string • Default: 'auto'Horizontal alignment of the text content.
Lacking native support:
- iOS:
'justify'is not supported by SwiftUI'sTextFieldand falls back to the default alignment.
Acceptable values are: 'auto' | 'center' | 'left' | 'right' | 'justify'
{
color: string,
fontFamily: string,
fontSize: number,
fontWeight: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900',
letterSpacing: number,
lineHeight: number,
textAlign: 'center' | 'left' | 'right'
}Text-level style — font, color, alignment, spacing.
Deprecated: The Android
TextInputrenders an unstyledBasicTextFieldthat has no underline indicator, so this has no effect. To draw your own border, pass it throughstyleormodifiers.
ColorValueColor of the underline indicator on Android.
ObservableState<string>An observable state holding the current text. Create one with
useNativeState('initial value') from @expo/ui.
Omit to let the field manage its own internal state.
Types
Imperative methods exposed via the TextInput ref.
| Property | Type | Description |
|---|---|---|
| blur | () => void | Programmatically blur the input. |
| clear | () => void | Clear the current text. |
| focus | () => void | Programmatically focus the input. |
| isFocused | () => boolean | Returns whether the input currently has focus. |
| setSelection | (start: number, end: number) => Promise<void> | Only for: iOS 18.0+ Programmatically set the selection range. |