One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Text Input

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.

Usage

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.

How it works

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.

API reference

Value and eventsTypeDescription
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.
stringThe 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) => voidThe 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) => voidThe return key was pressed, with the current text.
onFocus() => voidThe field gained focus.
() => voidThe field gained focus.
onBlur() => voidThe field lost focus.
() => voidThe field lost focus.
BehaviorTypeDescription
placeholderstringShown while the field is empty.
stringShown while the field is empty.
autoFocusbooleanFocus the field when it mounts.
booleanFocus the field when it mounts.
editableboolean = truefalse disables editing and focus.
boolean = truefalse disables editing and focus.
readOnlybooleantrue disables editing; wins over editable.
booleantrue disables editing; wins over editable.
multilinebooleanAllow line breaks. The field grows unless style sets a height.
booleanAllow line breaks. The field grows unless style sets a height.
maxLengthnumberThe most characters the field keeps.
numberThe most characters the field keeps.
secureTextEntrybooleanHide the text, for passwords.
booleanHide the text, for passwords.
styleStyleProp<ViewStyle>Size, margin, and position of the field.
StyleProp<ViewStyle>Size, margin, and position of the field.
KeyboardTypeDescription
keyboardTypeKeyboardTypeOptionsWhich keyboard opens, using React Native names.
KeyboardTypeOptionsWhich 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.
ReturnKeyTypeOptionsThe 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.
booleanfalse turns off autocorrection.
autoCompletestringAn HTML autocomplete token, such as email or one-time-code. iOS and web.
stringAn 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 onlyTypeDescription
numberOfLinesnumberVisible rows of a multiline field.
numberVisible rows of a multiline field.
rowsnumberSame as numberOfLines.
numberSame as numberOfLines.
onContentSizeChange(size: { width: number; height: number }) => voidThe scroll size of the text changed.
(size: { width: number; height: number }) => voidThe scroll size of the text changed.
onSelectionChange(selection: { start: number; end: number }) => voidThe user moved the selection.
(selection: { start: number; end: number }) => voidThe 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.
booleanSelect all the text on focus.
selectionColorColorValueSelection highlight, and caret when cursorColor is unset.
ColorValueSelection highlight, and caret when cursorColor is unset.
selectionHandleColorColorValueAccepted for React Native parity.
ColorValueAccepted for React Native parity.
cursorColorColorValueCaret color.
ColorValueCaret color.
caretHiddenbooleanHide the caret.
booleanHide the caret.
placeholderTextColorColorValuePlaceholder color.
ColorValuePlaceholder 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.
ColorValueAccepted for React Native parity; ignored everywhere.

The field also takes View props such as testID, nativeID, and accessibilityLabel.

RefTypeDescription
focus() => voidFocus the field. Does nothing when it is not editable.
() => voidFocus the field. Does nothing when it is not editable.
blur() => voidRemove focus and close the keyboard.
() => voidRemove focus and close the keyboard.
clear() => voidSet the text to an empty string.
() => voidSet the text to an empty string.
isFocused() => booleanWhether the field has focus right now.
() => booleanWhether 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.

Platform differences

The return key row covers single line fields.

iOSAndroidWeb
ControlSwiftUI TextField or SecureFieldMaterial 3 TextFieldinput or textarea
Default lookPlain text, no borderFilled, 56 tallGray border, 8px padding
Return keyAlways submitsSubmits, except next and previousSubmits, then blurs
Max lengthTrims after typingBlocks the keymaxlength attribute
AutoFill hintContent typeIgnoredautocomplete attribute
Text alignmentIgnoredAppliedApplied
Set selectionLogs a warningLogs a warningApplied

Limits and gotchas

Examples

Send a message and clear the field

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.

Sign in with AutoFill

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.

Show a live character count

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.

Focus a field from a button

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.