TextField
A SwiftUI TextField component for text input. Also called TextInput (Universal).
Expo UI TextField matches the official SwiftUI TextField API and supports single-line and multiline input, keyboard configuration, submit handling, and an imperative ref for programmatic control.
Usage
Uncontrolled text field
Bind a useNativeState observable to text. The field tracks the user's input on its own, and you read the current value from textState.value.
import { Host, TextField, useNativeState } from '@expo/ui/swift-ui';
export default function BasicTextFieldExample() {
const textState = useNativeState('');
// A text field stretches to the width it is given, so give the host a size.
return (
<Host style={{ flex: 1 }}>
<TextField placeholder="Username" text={textState} />
</Host>
);
}
Controlled text field
Pass an onTextChange worklet to transform or validate input and write the result back to the useNativeState observable state. The example below uppercases the text as it is typed.
Note: Worklets require installing
react-native-worklets.
import { Host, TextField, useNativeState } from '@expo/ui/swift-ui';
import { useCallback } from 'react';
export default function ControlledTextFieldExample() {
const text = useNativeState('');
const handleTextChange = useCallback(
(value: string) => {
'worklet';
text.value = value.toUpperCase();
},
[text]
);
return (
<Host style={{ flex: 1 }}>
<TextField
placeholder="Name"
text={text}
onTextChange={handleTextChange}
/>
</Host>
);
}
Multiline text field
Set axis="vertical" to allow the text field to expand vertically.
- Use the 1 modifier to control the visible line count.
- Give the
Hostan explicit size so the field has a width to expand within.
import { Host, TextField, useNativeState } from '@expo/ui/swift-ui';
import { lineLimit, fixedSize } from '@expo/ui/swift-ui/modifiers';
export default function MultilineTextFieldExample() {
const textState = useNativeState('');
return (
<Host style={{ flex: 1 }}>
<TextField
axis="vertical"
text={textState}
placeholder="Tell us about yourself..."
modifiers={[
lineLimit(5),
fixedSize({ horizontal: false, vertical: true }),
]}
/>
</Host>
);
}
Keyboard type
Use the keyboardType modifier to display a specific keyboard layout.
import { Host, TextField, useNativeState } from '@expo/ui/swift-ui';
import {
keyboardType,
autocorrectionDisabled,
} from '@expo/ui/swift-ui/modifiers';
export default function KeyboardTypeExample() {
const textState = useNativeState('');
return (
<Host style={{ flex: 1 }}>
<TextField
placeholder="Email"
text={textState}
modifiers={[
keyboardType('email-address'),
autocorrectionDisabled(),
]}
/>
</Host>
);
}
Submit handling
Use the submitLabel modifier to customize the return key and onSubmit to handle the submit action.
import { Host, TextField, useNativeState } from '@expo/ui/swift-ui';
import { submitLabel, onSubmit } from '@expo/ui/swift-ui/modifiers';
export default function SubmitHandlingExample() {
const textState = useNativeState('');
return (
<Host style={{ flex: 1 }}>
<TextField
placeholder="Search..."
text={textState}
modifiers={[
submitLabel('search'),
onSubmit(() =>
console.log('Submitted:', textState.value)
),
]}
/>
</Host>
);
}
Imperative ref
Use a ref to imperatively set text, focus, blur, or select text.
Note:
setSelectionrequires iOS 18.0+ / tvOS 18.0+. The other ref methods work on all supported versions.
import { useRef } from 'react';
import {
Host,
TextField,
TextFieldRef,
Button,
HStack,
VStack,
useNativeState,
} from '@expo/ui/swift-ui';
import { buttonStyle } from '@expo/ui/swift-ui/modifiers';
export default function ImperativeRefExample() {
const ref = useRef<TextFieldRef>(null);
const textState = useNativeState('Select me!');
return (
<Host style={{ flex: 1 }}>
<VStack spacing={12}>
<TextField
ref={ref}
text={textState}
placeholder="Imperative field"
/>
<HStack spacing={12}>
<Button
modifiers={[buttonStyle('bordered')]}
onPress={() => ref.current?.focus()}
label="Focus"
/>
<Button
modifiers={[buttonStyle('bordered')]}
onPress={() => ref.current?.blur()}
label="Blur"
/>
</HStack>
<HStack spacing={12}>
<Button
modifiers={[buttonStyle('bordered')]}
onPress={() => ref.current?.setText('SwiftUI rocks!')}
label="Set text"
/>
<Button
modifiers={[buttonStyle('bordered')]}
onPress={() => ref.current?.clear()}
label="Clear"
/>
<Button
modifiers={[buttonStyle('bordered')]}
onPress={() => ref.current?.setSelection(0, 7)}
label="Select"
/>
</HStack>
</VStack>
</Host>
);
}
Worklet text masking
When onTextChange is marked with the 'worklet' directive, it runs synchronously on the UI thread, so writes to 2 observables inside the callback take effect before the next frame.
- There is no flicker between the typed text and the masked text.
- The example below masks a phone number as the user types and writes both
textandselectionfrom the worklet to keep the cursor at the end of the formatted value.
Note: Worklets require installing
react-native-worklets. Theselectionprop requires iOS 18.0+ / tvOS 18.0+. On older versions the worklet can still update the text but cursor positioning is unavailable.
import { Host, TextField, useNativeState } from '@expo/ui/swift-ui';
import { keyboardType } from '@expo/ui/swift-ui/modifiers';
import { useEffectEvent } from 'react';
export default function WorkletPhoneMaskExample() {
const phone = useNativeState('');
const selection = useNativeState({ start: 0, end: 0 });
const handleTextChange = useEffectEvent((v: string) => {
'worklet';
const digits = v.replace(/\D/g, '').slice(0, 10);
let formatted = digits;
if (digits.length > 6) {
formatted = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
} else if (digits.length > 3) {
formatted = `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
}
if (formatted !== v) {
phone.value = formatted;
// Snaps to end for demo. Real masks need smarter cursor handling.
selection.value = {
start: formatted.length,
end: formatted.length,
};
}
});
return (
<Host style={{ flex: 1 }}>
<TextField
text={phone}
selection={selection}
placeholder="(555) 123-4567"
modifiers={[keyboardType('phone-pad')]}
onTextChange={handleTextChange}
/>
</Host>
);
}
API
import { TextField } from '@expo/ui/swift-ui';Component
TextField
Type: React.Element<TextFieldProps>
Renders a SwiftUI TextField.
TextFieldProps
autoFocus
Optional • Type: boolean • Default: false
If true, the text field will be focused automatically when mounted.
axis
Optional • Literal type: string • Default: 'horizontal'
The axis along which the text field grows when content exceeds a single line.
'horizontal'— single line (default).'vertical'— expands vertically for multiline content. UselineLimitmodifier to cap visible lines.
Acceptable values are: 'vertical' | 'horizontal'
children
Optional • Type: ReactNode
Slot children — supports <TextField.Placeholder> with a <Text> child (any text-styling modifiers on that Text are preserved as the placeholder's styling).
maxLength
Optional • Type: number
Maximum number of characters allowed. Truncates natively as the user types.
onFocusChange
Optional • Type: (focused: boolean) => void
A callback triggered when the field gains or loses focus.
onSelectionChange
iOS 18+Optional • Type: (selection: { end: number, start: number }) => void
A callback triggered when the text selection range changes.
onTextChange
Optional • Type: (text: string) => void
A callback triggered when the text value changes.
If the callback is marked with the 'worklet' directive, it runs synchronously on the UI thread; otherwise it is delivered asynchronously as a regular JS event.
placeholder
Optional • Type: string
A text that is displayed when the field is empty.
ref
Optional • Type: Ref<TextFieldRef>
selection
iOS 18+Optional • Type: ObservableState<TextFieldSelection>
Observable state the field writes the current selection to. Create with useNativeState<TextFieldSelection>({ start: 0, end: 0 }). Use ref.setSelection(start, end) to set programmatically.
text
Optional • Type: ObservableState<string>
An observable state that holds the current text. Create one with useNativeState('') or useNativeState('initial value'). If omitted, the field manages its own internal state.
Inherited props
Types
ObservableState
Observable state shared between JavaScript and native views (Jetpack Compose on Android and SwiftUI on iOS).
Type: SharedObject extended by:
| Property | Type | Description |
|---|---|---|
| onChange | [listener] | null | A single listener invoked on the native UI runtime whenever the value changes (after iOS didSet and Android's setter). Assigning replaces the previous listener; assign null to clear. The initial value does not fire onChange. The callback must be a worklet so it can run synchronously on the UI thread. Attach it inside useEffect and clear it in the cleanup so the listener lifecycle matches the component lifecycle. Example |
const state = useNativeState(0);
useEffect(() => {
state.onChange = (value) => {
'worklet';
console.log('changed to', value);
};
}, []);| | value | T | The current value.
- Writes from a UI worklet are synchronous and immediately readable.
- Writes from the JS thread are scheduled to the UI thread asynchronously, the new value is not readable until the update has been applied.
- Prefer writing from a worklet when you need synchronous updates | | get |
() => T| Reads the current value. - A React Compiler compliant alternative to reading
.value| | set |(value: T) => void| Writes a new value. - A React Compiler-compliant alternative to assigning
.value|
TextFieldRef
Can be used for imperatively focusing and setting text/selection on the TextField component.
| Property | Type | Description |
|---|---|---|
| blur | () => Promise<void> | - |
| clear | () => Promise<void> | Clear the current text. |
| focus | () => Promise<void> | - |
| setSelection | (start: number, end: number) => Promise<void> | iOS 18+. Programmatically set the selection range. |
| setText | (newText: string) => Promise<void> | - |
TextFieldSelection
Selection range — start and end are character offsets into the field's text.
| Property | Type | Description |
|---|---|---|
| end | number | - |
| start | number | - |
Modifiers
Use these modifiers in the modifiers prop of the SwiftUI and Universal version.
Expo UI lets you put any of the 89 modifiers for any view on TextField, but not all of them do something here.
- 70 change what you see on TextField.
- 6 have no visible effect on TextField.
- 13 work only in one place, such as a widget or a
Grid.
Best modifiers for TextField
These modifiers are made for TextField, or they are the ones that you use most with it.
Also works on TextField
TextField takes these modifiers too, and each one works in the usual way.
accessibilityAddTraitsaccessibilityHiddenaccessibilityHintaccessibilityIdentifieraccessibilityInputLabelsaccessibilityLabelaccessibilityRemoveTraitsaccessibilityValueallowsTighteninganimationaspectRatiobackgroundOverlayblurboldborderbrightnessclippedclipShapecolorInvertcontainerRelativeFrameiOS 17+contentShapecontentTransitioniOS 16+contrastcornerRadiusdynamicTypeSizeenvironmentfixedSizeforegroundColorglassEffectgrayscalehiddenhueRotationidignoreSafeAreaitalickerninglineHeightiOS 26+lineLimitlineSpacingluminanceToAlphamaskminimumScaleFactormonospacedDigitmultilineTextAlignmentoffsetonAppearonDisappearonGeometryChangeonLongPressGestureonTapGestureopacityoverlayredactedrotation3DEffectrotationEffectsaturationscaleEffectshadowstrikethroughstrokeBordertextCasetinttruncationModeunderline
No visible effect on TextField
TextField accepts these modifiers, but you see no change. They do not give an error.
| Modifier | Why nothing happens |
|---|---|
accessibilityElement | It joins the child views into one accessibility element, and TextField takes no child views. |
containerShape | It sets the shape that the child views use for containerRelativeShape, and TextField takes no child views. |
geometryGroup iOS 17+ | It works on the child views that you give the component, and TextField takes none. |
imageScale | It changes only SF Symbols, and TextField shows none. |
layoutPriority | It has an effect only when the view shares an axis with other views in a stack. |
zIndex | It changes the order of views that cover each other in the same parent, and TextField does not stack its children. |
Only in some places
These modifiers work only in a widget, in a Grid, in a Namespace or in a parent that asks for them.
| Modifier | Where it works |
|---|---|
activityBackgroundTint | Works only in a Live Activity. It has no effect in the app. |
alignmentGuide | Has an effect only when the parent, such as a VStack or ZStack, aligns its children by the same guide. |
containerBackground | Works only in a widget, or in a navigation container when you use a navigation placement. |
glassEffectId | Works only on a view that also has glassEffect, in a GlassEffectContainer and a Namespace. |
gridCellAnchor iOS 16+ | Works only on a cell of a Grid. Grid is in @expo/ui/swift-ui, but it has no Expo docs page. |
gridCellColumns | Works only on a view in a Grid.Row. Grid is in @expo/ui/swift-ui, but it has no Expo docs page. |
gridCellUnsizedAxes | Works only on a view in a Grid. Grid is in @expo/ui/swift-ui, but it has no Expo docs page. |
gridColumnAlignment iOS 16+ | Works only on a view in a Grid.Row. Grid is in @expo/ui/swift-ui, but it has no Expo docs page. |
invalidatableContent iOS 17+ | Has an effect only when a parent view uses redacted with the invalidated reason. |
matchedGeometryEffect | Works only in a Namespace. Give two views the same id and change between them with an animation. |
privacySensitive | Has an effect only when a parent view uses redacted with the privacy reason. |
unredacted | Has an effect only in a parent view that uses redacted. |
widgetURL | Works only in a widget. It has no effect in the app. |
Sources
The original Expo docs for this page:
- TextField (SwiftUI) (
source-of-truth/versions/v57.0.0/sdk/ui/swift-ui/textfield.md) - TextField (Universal) (
source-of-truth/versions/v57.0.0/sdk/ui/universal/textinput.md)






