One text field for iOS, Android, and web, drawn by each platform's own control
One.UI.TextInput is the text field to reach for when the same screen runs on
more than one platform. It renders the platform’s own control: a SwiftUI
TextField on iOS, a Material 3 TextField from Jetpack Compose on Android,
and an <input> or <textarea> on web. You get native editing, AutoFill,
keyboards, and accessibility, with one set of props.
For options only one platform has, use the platform field directly: One.iOS.TextField for SwiftUI field styles and form labels, or One.Android.TextField for the outlined Material variant.
import { One, useNativeState } from 'one'
function NameField() { const name = useNativeState('')
return <One.UI.TextInput value={name} placeholder="Name" />}value takes a handle from useNativeState, not a string. Read the text with
name.value and replace it with name.set(text). Leave value out and pass
defaultValue for a field you only read on submit.
The text lives in native memory, not in React state. useNativeState creates
an entry in One’s native registry and returns a handle that carries its id.
The field binds to that entry by id, and so does any other view you pass the
same handle to.
When the user types, the native field shows the character and writes the entry
first. Then it sends an event to JavaScript, which calls onChangeText and
re-renders the component that owns the handle. Typing never waits for
JavaScript, so a busy JS thread cannot drop characters or move the caret.
When your code calls name.set(text), the write is synchronous. On iOS it runs
on the main thread and returns once SwiftUI has the new text, so the field is
already updated on the next line of your code. On Android it writes Compose
snapshot state, which is safe from any thread.
On iOS and Android the field fills its parent’s width and sizes its own
height: the SwiftUI view or Compose node measures itself and reports the
height to React Native’s layout, with a minimum of 56 on Android. A
multiline field grows a line at a time. Give it a height in style to make
it scroll inside a fixed box instead. On web it is a plain <input> or
<textarea>, sized by style like any element.
| Value and events | Type | Description |
|---|---|---|
valueNativeState<string>The text, as a handle from useNativeState. Typing writes it directly. | NativeState<string> | The text, as a handle from useNativeState. Typing writes it directly. |
defaultValuestringThe starting text when value is left out. Read once, at mount. | string | The starting text when value is left out. Read once, at mount. |
onChangeText(text: string) => voidThe user edited the text. Writes from your code do not call it. | (text: string) => void | The user edited the text. Writes from your code do not call it. |
onSubmitEditing(text: string) => voidThe return key was pressed, with the current text. | (text: string) => void | The return key was pressed, with the current text. |
onFocus() => voidThe field gained focus. | () => void | The field gained focus. |
onBlur() => voidThe field lost focus. | () => void | The field lost focus. |
| Behavior | Type | Description |
|---|---|---|
placeholderstringShown while the field is empty. | string | Shown while the field is empty. |
autoFocusbooleanFocus the field when it mounts. | boolean | Focus the field when it mounts. |
editableboolean = truefalse disables editing and focus. | boolean = true | false disables editing and focus. |
readOnlybooleantrue disables editing; wins over editable. | boolean | true disables editing; wins over editable. |
multilinebooleanAllow line breaks. The field grows unless style sets a height. | boolean | Allow line breaks. The field grows unless style sets a height. |
maxLengthnumberThe most characters the field keeps. | number | The most characters the field keeps. |
secureTextEntrybooleanHide the text, for passwords. | boolean | Hide the text, for passwords. |
styleStyleProp<ViewStyle>Size, margin, and position of the field. | StyleProp<ViewStyle> | Size, margin, and position of the field. |
| Keyboard | Type | Description |
|---|---|---|
keyboardTypeKeyboardTypeOptionsWhich keyboard opens, using React Native names. | KeyboardTypeOptions | Which keyboard opens, using React Native names. |
inputMode"decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url"The web name for the keyboard. keyboardType wins when both are set. | "decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | The web name for the keyboard. keyboardType wins when both are set. |
returnKeyTypeReturnKeyTypeOptionsThe title of the return key. | ReturnKeyTypeOptions | The title of the return key. |
enterKeyHint"enter" | "done" | "go" | "next" | "previous" | "search" | "send"The web name for the return key. returnKeyType wins when both are set. | "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | The web name for the return key. returnKeyType wins when both are set. |
autoCapitalize"none" | "sentences" | "words" | "characters"When the keyboard turns on shift. | "none" | "sentences" | "words" | "characters" | When the keyboard turns on shift. |
autoCorrectbooleanfalse turns off autocorrection. | boolean | false turns off autocorrection. |
autoCompletestringAn HTML autocomplete token, such as email or one-time-code. iOS and web. | string | An HTML autocomplete token, such as email or one-time-code. iOS and web. |
textAlign"auto" | "left" | "right" | "center" | "justify"Horizontal text alignment. Android and web. | "auto" | "left" | "right" | "center" | "justify" | Horizontal text alignment. Android and web. |
These props apply on web. iOS and Android accept them and ignore them, so shared code can pass them freely.
| Web only | Type | Description |
|---|---|---|
numberOfLinesnumberVisible rows of a multiline field. | number | Visible rows of a multiline field. |
rowsnumberSame as numberOfLines. | number | Same as numberOfLines. |
onContentSizeChange(size: { width: number; height: number }) => voidThe scroll size of the text changed. | (size: { width: number; height: number }) => void | The scroll size of the text changed. |
onSelectionChange(selection: { start: number; end: number }) => voidThe user moved the selection. | (selection: { start: number; end: number }) => void | The user moved the selection. |
selectionNativeState<TextInputSelection>The selection, as a handle you can read and set. | NativeState<TextInputSelection> | The selection, as a handle you can read and set. |
selectTextOnFocusbooleanSelect all the text on focus. | boolean | Select all the text on focus. |
selectionColorColorValueSelection highlight, and caret when cursorColor is unset. | ColorValue | Selection highlight, and caret when cursorColor is unset. |
selectionHandleColorColorValueAccepted for React Native parity. | ColorValue | Accepted for React Native parity. |
cursorColorColorValueCaret color. | ColorValue | Caret color. |
caretHiddenbooleanHide the caret. | boolean | Hide the caret. |
placeholderTextColorColorValuePlaceholder color. | ColorValue | Placeholder color. |
textStyleStyleProp<TextStyle>Font, size, and color of the text. | StyleProp<TextStyle> | Font, size, and color of the text. |
underlineColorAndroidColorValueAccepted for React Native parity; ignored everywhere. | ColorValue | Accepted for React Native parity; ignored everywhere. |
The field also takes View props such as testID, nativeID, and
accessibilityLabel.
| Ref | Type | Description |
|---|---|---|
focus() => voidFocus the field. Does nothing when it is not editable. | () => void | Focus the field. Does nothing when it is not editable. |
blur() => voidRemove focus and close the keyboard. | () => void | Remove focus and close the keyboard. |
clear() => voidSet the text to an empty string. | () => void | Set the text to an empty string. |
isFocused() => booleanWhether the field has focus right now. | () => boolean | Whether the field has focus right now. |
setSelection(start: number, end?: number) => Promise<void>Select a range. Web only; native logs a warning. | (start: number, end?: number) => Promise<void> | Select a range. Web only; native logs a warning. |
The return key row covers single line fields.
| iOS | Android | Web | |
|---|---|---|---|
| Control | SwiftUI TextField or SecureField | Material 3 TextField | input or textarea |
| Default look | Plain text, no border | Filled, 56 tall | Gray border, 8px padding |
| Return key | Always submits | Submits, except next and previous | Submits, then blurs |
| Max length | Trims after typing | Blocks the key | maxlength attribute |
| AutoFill hint | Content type | Ignored | autocomplete attribute |
| Text alignment | Ignored | Applied | Applied |
| Set selection | Logs a warning | Logs a warning | Applied |
style sizes and places the field on every
platform, but font, size, and color of the text come from the platform
default on iOS and Android. textStyle applies only on web.name.set() and ref.clear() update the
field without calling onChangeText, which reports only what the user typed.next does not submit on Android. Compose runs its own next and previous
actions, so onSubmitEditing never fires for those return keys there. Use
done or go on the last field of a form.maxLength after the fact. The SwiftUI field has no length
limit of its own, so an extra character can show for a frame before One
writes the trimmed text back.SecureField has no
vertical axis, so multiline has no effect with secureTextEntry.defaultValue is read once. Changing it later does nothing. Pass a
handle as value and call set to change the text from code.import { One, useNativeState } from 'one'
function Composer({ onSend }: { onSend: (text: string) => void }) { const draft = useNativeState('')
return ( <One.UI.TextInput value={draft} placeholder="Message" returnKeyType="send" onSubmitEditing={(text) => { if (!text.trim()) return onSend(text) draft.set('') }} /> )}The handler receives the submitted text, so it needs no read of draft.
draft.set('') empties the field at once, on every platform.
import { One, useNativeState } from 'one'
function SignIn({ signIn }: { signIn: () => void }) { const email = useNativeState('') const password = useNativeState('')
return ( <> <One.UI.TextInput value={email} placeholder="Email" keyboardType="email-address" autoCapitalize="none" autoComplete="username" /> <One.UI.TextInput value={password} placeholder="Password" secureTextEntry autoComplete="current-password" returnKeyType="go" onSubmitEditing={signIn} /> </> )}On iOS, username and current-password set each field’s AutoFill content
type, marking the pair as a login form. Web passes the tokens to the
autocomplete attribute. Android ignores autoComplete.
import { Text } from 'react-native'import { One, useNativeState } from 'one'
function Bio() { const bio = useNativeState('')
return ( <> <One.UI.TextInput value={bio} multiline maxLength={160} style={{ height: 120 }} /> <Text>{bio.value.length} / 160</Text> </> )}The component that calls useNativeState subscribes to the handle, so it
re-renders after each keystroke and bio.value is always current.
import { useRef, type ComponentRef } from 'react'import { Button } from 'react-native'import { One } from 'one'
function Search() { const input = useRef<ComponentRef<typeof One.UI.TextInput>>(null)
return ( <> <One.UI.TextInput ref={input} placeholder="Search" returnKeyType="search" /> <Button title="Search" onPress={() => input.current?.focus()} /> </> )}onFocus runs as soon as focus() is called, once per change, without
waiting for the native field to confirm.
Edit this page on GitHub.