This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.

TextField

Jetpack Compose TextField components for native Material3 text input.

Android
Included in Expo Go
Recommended version:
~56.0.26

Expo UI provides three text field components that match the official Jetpack Compose TextField API: TextField (filled), OutlinedTextField (outlined border), and BasicTextField (unstyled). The Material variants TextField and OutlinedTextField share the same props and support composable slot children for label, placeholder, icons, prefix, suffix, and supporting text. BasicTextField has no Material chrome, so you supply your own decoration.

TypeAppearancePurpose
FilledSolid background with a bottom indicator line.Default text input style following Material3 design. Use for most forms and input fields.
OutlinedTransparent background with a border outline.Alternative style that provides a distinct visual boundary. Use when filled fields blend into the background.
BasicNo container, indicator, or padding. Just the editable text.Fully custom-styled inputs. Style it yourself and add decoration through DecorationBox.
Filled, outlined, and basic (unstyled) text fieldsFilled, outlined, and basic (unstyled) text fields

Installation

Terminal
- npx expo install @expo/ui
- yarn expo install @expo/ui
- pnpm expo install @expo/ui
- bun expo install @expo/ui

If you are installing this in an existing React Native app, make sure to install expo in your project.

Usage

Uncontrolled text field

Bind a useNativeState observable to value. The field tracks the user's input on its own, and you read the current value from text.value. The filled style shown here is the default Material3 text input.

UncontrolledTextFieldExample.tsx
import { Host, TextField, Text, useNativeState } from '@expo/ui/jetpack-compose'; export default function UncontrolledTextFieldExample() { const text = useNativeState(''); return ( <Host matchContents> <TextField value={text}> <TextField.Label> <Text>Username</Text> </TextField.Label> </TextField> </Host> ); }

Controlled text field

Pass a useNativeState observable as value and an onValueChange worklet to transform or validate input before writing it back. The example below uppercases the text as it is typed.

ControlledTextFieldExample.tsx
import { Host, TextField, Text, useNativeState } from '@expo/ui/jetpack-compose'; import { useCallback } from 'react'; export default function ControlledTextFieldExample() { const text = useNativeState(''); const handleValueChange = useCallback( (value: string) => { 'worklet'; text.value = value.toUpperCase(); }, [text] ); return ( <Host matchContents> <TextField value={text} onValueChange={handleValueChange}> <TextField.Label> <Text>Name</Text> </TextField.Label> </TextField> </Host> ); }

Outlined text field

Use OutlinedTextField for a text field with a border outline instead of a filled background.

OutlinedTextFieldExample.tsx
import { Host, OutlinedTextField, Text, useNativeState } from '@expo/ui/jetpack-compose'; export default function OutlinedTextFieldExample() { const text = useNativeState(''); return ( <Host matchContents> <OutlinedTextField value={text}> <OutlinedTextField.Label> <Text>Email</Text> </OutlinedTextField.Label> <OutlinedTextField.Placeholder> <Text>you@example.com</Text> </OutlinedTextField.Placeholder> </OutlinedTextField> </Host> ); }

Basic text field

BasicTextField is the unstyled Compose primitive, with no container, indicator, or padding. Style it yourself with modifiers and supply decoration through DecorationBox, placing InnerTextField where the editable text should render. Wrap placeholder content in Placeholder to have it shown only while the field is empty, toggled natively from the field's text.

BasicTextFieldExample.tsx
import { Host, BasicTextField, Box, Text, useNativeState } from '@expo/ui/jetpack-compose'; import { background, clip, fillMaxWidth, padding, Shapes, } from '@expo/ui/jetpack-compose/modifiers'; export default function BasicTextFieldExample() { const value = useNativeState(''); return ( <Host matchContents> <BasicTextField cursorColor="#7c3aed" value={value} modifiers={[ fillMaxWidth(), clip(Shapes.RoundedCorner(12)), background('#f3f4f6'), padding(12, 10, 12, 10), ]}> <BasicTextField.DecorationBox> <Box> <BasicTextField.Placeholder> <Text color="#9ca3af">Search…</Text> </BasicTextField.Placeholder> <BasicTextField.InnerTextField /> </Box> </BasicTextField.DecorationBox> </BasicTextField> </Host> ); }

Slots

Both TextField and OutlinedTextField support 7 composable slots that match the Compose API: Label, Placeholder, LeadingIcon, TrailingIcon, Prefix, Suffix, and SupportingText.

TextFieldSlotsExample.tsx
import { Host, TextField, Text, useNativeState } from '@expo/ui/jetpack-compose'; export default function TextFieldSlotsExample() { const text = useNativeState(''); return ( <Host matchContents> <TextField value={text}> <TextField.Label> <Text>Price</Text> </TextField.Label> <TextField.Placeholder> <Text>0.00</Text> </TextField.Placeholder> <TextField.LeadingIcon> <Text>💰</Text> </TextField.LeadingIcon> <TextField.Prefix> <Text>$</Text> </TextField.Prefix> <TextField.Suffix> <Text>USD</Text> </TextField.Suffix> <TextField.SupportingText> <Text>Enter the amount</Text> </TextField.SupportingText> </TextField> </Host> ); }

Keyboard options

Use the keyboardOptions prop to configure the keyboard type, capitalization, auto-correct, and IME action.

KeyboardOptionsExample.tsx
import { Host, TextField, Text, useNativeState } from '@expo/ui/jetpack-compose'; export default function KeyboardOptionsExample() { const text = useNativeState(''); return ( <Host matchContents> <TextField value={text} singleLine keyboardOptions={{ keyboardType: 'email', capitalization: 'none', autoCorrectEnabled: false, imeAction: 'done', }}> <TextField.Label> <Text>Email</Text> </TextField.Label> </TextField> </Host> ); }

Keyboard actions

Use the keyboardActions prop to handle IME action button presses. The triggered callback depends on the imeAction set in keyboardOptions. Each callback receives the current text value.

KeyboardActionsExample.tsx
import { Host, TextField, Text, useNativeState } from '@expo/ui/jetpack-compose'; export default function KeyboardActionsExample() { const text = useNativeState(''); return ( <Host matchContents> <TextField value={text} singleLine keyboardOptions={{ imeAction: 'search' }} keyboardActions={{ onSearch: value => console.log('Searched:', value), }}> <TextField.Label> <Text>Search</Text> </TextField.Label> </TextField> </Host> ); }

Imperative ref

Use a ref to imperatively set text, clear the field, change the selection, or move focus.

ImperativeRefExample.tsx
import { useRef } from 'react'; import { Host, TextField, TextFieldRef, Button, Row, Text, Column, useNativeState, } from '@expo/ui/jetpack-compose'; import { padding } from '@expo/ui/jetpack-compose/modifiers'; export default function ImperativeRefExample() { const ref = useRef<TextFieldRef>(null); const text = useNativeState(''); return ( <Host matchContents> <Column> <TextField ref={ref} value={text} singleLine> <TextField.Label> <Text>Name</Text> </TextField.Label> </TextField> <Row horizontalArrangement={{ spacedBy: 8 }} modifiers={[padding(8, 0, 0, 0)]}> <Button onClick={() => ref.current?.setText('Hello world')}> <Text>Set text</Text> </Button> <Button onClick={() => ref.current?.clear()}> <Text>Clear</Text> </Button> <Button onClick={() => ref.current?.setSelection(0, 5)}> <Text>Select first word</Text> </Button> </Row> <Row horizontalArrangement={{ spacedBy: 8 }} modifiers={[padding(8, 0, 0, 0)]}> <Button onClick={() => ref.current?.focus()}> <Text>Focus</Text> </Button> <Button onClick={() => ref.current?.blur()}> <Text>Blur</Text> </Button> </Row> </Column> </Host> ); }

Worklet text masking

When onValueChange is marked with the 'worklet' directive, it runs synchronously on the UI thread, so writes to 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 value and selection from the worklet to keep the cursor at the end of the formatted value.

WorkletPhoneMaskExample.tsx
import { Host, TextField, Text, useNativeState } from '@expo/ui/jetpack-compose'; import { fillMaxWidth } from '@expo/ui/jetpack-compose/modifiers'; import { useCallback } from 'react'; export default function WorkletPhoneMaskExample() { const phone = useNativeState(''); const selection = useNativeState({ start: 0, end: 0 }); const handleValueChange = useCallback( (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 }; } }, [phone, selection] ); return ( <Host matchContents> <TextField value={phone} selection={selection} keyboardOptions={{ keyboardType: 'phone' }} modifiers={[fillMaxWidth()]} onValueChange={handleValueChange}> <TextField.Placeholder> <Text>(555) 123-4567</Text> </TextField.Placeholder> </TextField> </Host> ); }

API

import { TextField, OutlinedTextField, BasicTextField } from '@expo/ui/jetpack-compose';

No API data file found, sorry!