---
modificationDate: May 06, 2026
title: TextField
description: A SwiftUI TextField component for text input.
sourceCodeUrl: 'https://github.com/expo/expo/tree/sdk-56/packages/expo-ui'
packageName: '@expo/ui'
platforms: ['ios', 'tvos', 'expo-go']
---

<AgentInstructions>

## Submitting Feedback

If this page contains errors, outdated information, or gaps that blocked you from completing a task, report it so the docs can be improved:

curl -X POST https://api.expo.dev/v2/feedback/docs-send -H 'Content-Type: application/json' -d '{"url":"/versions/v56.0.0/sdk/ui/swift-ui/textfield/","feedback":"🤖 Agent feedback: <specific, actionable description>"}'

Only submit when you have something specific and actionable to report.

</AgentInstructions>

# TextField

A SwiftUI TextField component for text input.
iOS, tvOS, Included in Expo Go

> For the complete documentation index, see [llms.txt](/llms.txt). Use this file to discover all available pages.

Expo UI TextField matches the official SwiftUI [TextField API](https://developer.apple.com/documentation/swiftui/textfield) and supports single-line and multiline input, keyboard configuration, submit handling, and an imperative `ref` for programmatic control.

## Installation

```sh
npx expo install @expo/ui
```

If you are installing this in an [existing React Native app](/bare/overview), make sure to [install `expo`](/bare/installing-expo-modules) in your project.

## Usage

### Basic text field

```tsx
import { useState } from 'react';
import { Host, TextField } from '@expo/ui/swift-ui';

export default function BasicTextFieldExample() {
  const [value, setValue] = useState('');

  return (
    <Host matchContents>
      <TextField placeholder="Username" onTextChange={setValue} />
    </Host>
  );
}
```

### Multiline text field

Set `axis="vertical"` to allow the text field to expand vertically. Use the [`lineLimit`](/versions/v56.0.0/sdk/ui/swift-ui/modifiers#linelimit) modifier to control the visible line count. When using `Host matchContents`, add `fixedSize({ horizontal: false, vertical: true })` so the text field accepts the parent's width while using its ideal height.

```tsx
import { useState } from 'react';
import { Host, TextField } from '@expo/ui/swift-ui';
import { lineLimit, fixedSize } from '@expo/ui/swift-ui/modifiers';

export default function MultilineTextFieldExample() {
  const [value, setValue] = useState('');

  return (
    <Host matchContents>
      <TextField
        axis="vertical"
        placeholder="Tell us about yourself..."
        onTextChange={setValue}
        modifiers={[lineLimit(5), fixedSize({ horizontal: false, vertical: true })]}
      />
    </Host>
  );
}
```

### Keyboard type

Use the [`keyboardType`](/versions/v56.0.0/sdk/ui/swift-ui/modifiers#keyboardtypekeyboardtype) modifier to display a specific keyboard layout.

```tsx
import { useState } from 'react';
import { Host, TextField } from '@expo/ui/swift-ui';
import { keyboardType, autocorrectionDisabled } from '@expo/ui/swift-ui/modifiers';

export default function KeyboardTypeExample() {
  const [value, setValue] = useState('');

  return (
    <Host matchContents>
      <TextField
        placeholder="Email"
        onTextChange={setValue}
        modifiers={[keyboardType('email-address'), autocorrectionDisabled()]}
      />
    </Host>
  );
}
```

### Submit handling

Use the [`submitLabel`](/versions/v56.0.0/sdk/ui/swift-ui/modifiers#submitlabelsubmitlabel) modifier to customize the return key and [`onSubmit`](/versions/v56.0.0/sdk/ui/swift-ui/modifiers#onsubmithandler) to handle the submit action.

```tsx
import { useState } from 'react';
import { Host, TextField } from '@expo/ui/swift-ui';
import { submitLabel, onSubmit } from '@expo/ui/swift-ui/modifiers';

export default function SubmitHandlingExample() {
  const [value, setValue] = useState('');

  return (
    <Host matchContents>
      <TextField
        placeholder="Search..."
        onTextChange={setValue}
        modifiers={[submitLabel('search'), onSubmit(() => console.log('Submitted:', value))]}
      />
    </Host>
  );
}
```

### Imperative ref

Use a `ref` to imperatively set text, focus, blur, or select text.

> **Note:** `setSelection` requires iOS 18.0+ / tvOS 18.0+. The other ref methods work on all supported versions.

```tsx
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 text = useNativeState('Select me!');

  return (
    <Host matchContents>
      <VStack>
        <TextField ref={ref} text={text} 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"
          />
          <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 [`useNativeState`](/versions/v56.0.0/sdk/ui/swift-ui/usenativestate) 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 `text` and `selection` from the worklet to keep the cursor at the end of the formatted value.

> **Note:** Worklets require installing [`react-native-reanimated`](https://docs.swmansion.com/react-native-reanimated/) and [`react-native-worklets`](https://docs.swmansion.com/react-native-worklets/). The `selection` prop requires iOS 18.0+ / tvOS 18.0+. On older versions the worklet can still update the text but cursor positioning is unavailable.

```tsx
import { Host, TextField, useNativeState } from '@expo/ui/swift-ui';
import { keyboardType } from '@expo/ui/swift-ui/modifiers';

export default function WorkletPhoneMaskExample() {
  const phone = useNativeState('');
  const selection = useNativeState({ start: 0, end: 0 });

  return (
    <Host matchContents>
      <TextField
        text={phone}
        selection={selection}
        placeholder="(555) 123-4567"
        modifiers={[keyboardType('phone-pad')]}
        onTextChange={v => {
          '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;
            selection.value = { start: formatted.length, end: formatted.length };
          }
        }}
      />
    </Host>
  );
}
```

## API

```tsx
import { TextField } from '@expo/ui/swift-ui';
```

## Component

### `TextField`

Supported platforms: iOS, tvOS.

Type: React.[Element](https://www.typescriptlang.org/docs/handbook/jsx.html#function-component)<[TextFieldProps](#textfieldprops)\>

Renders a SwiftUI `TextField`.

TextFieldProps

### `autoFocus`

Supported platforms: iOS, tvOS.

Optional • Type: `boolean` • Default: `false`

If true, the text field will be focused automatically when mounted.

### `axis`

Supported platforms: iOS, tvOS.

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. Use `lineLimit` modifier to cap visible lines.

Acceptable values are: `'horizontal'` | `'vertical'`

### `children`

Supported platforms: iOS, tvOS.

Optional • Type: `React.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`

Supported platforms: iOS, tvOS.

Optional • Type: `number`

Maximum number of characters allowed. Truncates natively as the user types.

### `onFocusChange`

Supported platforms: iOS, tvOS.

Optional • Type: `(focused: boolean) => void`

A callback triggered when the field gains or loses focus.

### `onSelectionChange`

Supported platforms: iOS 18.0+ tvos 18.0+.

Optional • Type: `(selection: { end: number, start: number }) => void`

A callback triggered when the text selection range changes.

### `onTextChange`

Supported platforms: iOS, tvOS.

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`

Supported platforms: iOS, tvOS.

Optional • Type: `string`

A text that is displayed when the field is empty.

### `ref`

Supported platforms: iOS, tvOS.

Optional • Type: Ref<[TextFieldRef](#textfieldref)\>

### `selection`

Supported platforms: iOS 18.0+ tvos 18.0+.

Optional • Type: [ObservableState](#observablestate)<[TextFieldSelection](#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`

Supported platforms: iOS, tvOS.

Optional • Type: [ObservableState](#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

-   [CommonViewModifierProps](/versions/v56.0.0/sdk/ui/swift-ui/modifiers)

## Types

### `ObservableState`

Supported platforms: iOS, tvOS.

Observable state shared between JavaScript and native views (Jetpack Compose on Android and SwiftUI on iOS).

Type: [SharedObject](/versions/unversioned/sdk/expo#sharedobjecttype) extended by:

| Property | Type | Description |
| --- | --- | --- |
| value | `T` | The current value. Reads are safe from any thread; prefer writing from a worklet so the update runs on the native UI thread. Updating state from the JS thread might show a development warning. |

### `TextFieldRef`

Supported platforms: iOS, tvOS.

Can be used for imperatively focusing and setting text/selection on the `TextField` component.

| Property | Type | Description |
| --- | --- | --- |
| blur | () => [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<void\> | - |
| clear | () => [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<void\> | Clear the current text. |
| focus | () => [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<void\> | - |
| setSelection | (start: number, end: number) => [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<void\> | Supported platforms: iOS 18.0+ tvos 18.0+. Programmatically set the selection range. |
| setText | (newText: string) => [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<void\> | - |

### `TextFieldSelection`

Supported platforms: iOS, tvOS.

Selection range — `start` and `end` are character offsets into the field's text.

| Property | Type | Description |
| --- | --- | --- |
| end | `number` | - |
| start | `number` | - |
