This is documentation for the next SDK version. For up-to-date documentation, see the latest version (SDK 55).
TextInput
A text input backed by native SwiftUI and Jetpack Compose components, with a React Native-compatible API.
For the complete documentation index, see llms.txt. Use this file to discover all available pages.
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.
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
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
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
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.
Note: Worklets require installing
react-native-reanimatedandreact-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> ); }
Unsupported React Native props
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';
No API data file found, sorry!