Elements
Input
<Input> is a native text field: a UITextField (single line) or UITextView (multiline) on iOS and Mac Catalyst, an
EditText on Android. Typing, the caret, selection, autocorrection and autofill are the platform’s own.
import { Input, mount, signal, Text, View, type YNode } from "nsh";
import { ui } from "./palette";
function App() {
const name = signal("");
const password = signal("");
const notes = signal("");
const submitted = signal("");
let passwordInput!: YNode;
const field = () => ({ ...ui().inset, fontSize: 16, color: ui().text, borderRadius: 12, paddingHorizontal: 12, paddingVertical: 10 });
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 10 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>input</Text>
<Text style={{ color: ui().muted }}>Name (controlled, maxLength 20)</Text>
<Input
style={field()}
value={name()}
onChangeText={(text) => name.set(text)}
placeholder="Ada Lovelace"
placeholderTextColor={ui().faint}
autoCapitalize="words"
maxLength={20}
returnKeyType="next"
onSubmitEditing={() => passwordInput.focus()}
/>
<Text style={{ fontSize: 12, color: ui().muted }}>{name().length} / 20</Text>
<Text style={{ color: ui().muted }}>Password</Text>
<Input
ref={passwordInput}
style={field()}
value={password()}
onChangeText={(text) => password.set(text)}
secureTextEntry
autoComplete="current-password"
returnKeyType="done"
onSubmitEditing={() => submitted.set(`Signed in as ${name() || "nobody"}`)}
/>
<Text style={{ color: ui().muted }}>Notes (multiline)</Text>
<Input
style={{ ...field(), height: 100 }}
multiline
value={notes()}
onChangeText={(text) => notes.set(text)}
placeholder="Anything else?"
placeholderTextColor={ui().faint}
/>
<View
onPress={() => submitted.set(`Signed in as ${name() || "nobody"}`)}
activeOpacity={0.7}
disabled={password().length === 0}
class="disabled:opacity-40"
style={{
...ui().primary,
borderRadius: 10,
padding: 14,
alignItems: "center",
marginTop: 6,
}}
>
<Text style={{ color: ui().onPrimary, fontSize: 16, fontWeight: "600" }}>Sign in</Text>
</View>
<Text style={{ color: ui().up }}>{submitted()}</Text>
</View>
);
}
mount(App);import { Input, signal } from "nsh";
function NameField() {
const name = signal("");
return (
<Input
value={name()}
onChangeText={(text) => name.set(text)}
placeholder="Your name"
autoCapitalize="words"
returnKeyType="done"
style={{ fontSize: 16, padding: 12, borderWidth: 1, borderColor: "#d0d7de", borderRadius: 8 }}
/>
);
}
#Controlled and uncontrolled
- Controlled: pass
valueand update it inonChangeText. JS can rewrite what the user typed (trim it, format it): the text shows the edit at once and is corrected when the newvaluearrives. Each native edit carries a count, and avaluefrom JS is applied only if it is based on the latest edit, so a slow JS turn never overwrites newer typing. - Uncontrolled: pass
defaultValue(or nothing) and read the text fromonChangeTextoronSubmitEditing.
Rules that must hold while the user types are props, answered natively without asking JS: maxLength limits the length
and submitBehavior (or blurOnSubmit) decides what return does. See forms and the keyboard.
#Props
Besides the common props (with style typed as TextStyle: font, color, alignment and the
view styles apply):
#Text
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | The text, controlled by JS. | |
defaultValue | string | The initial text. | |
placeholder | string | Shown while the input is empty. | |
placeholderTextColor | ColorValue | The placeholder’s color. | |
multiline | boolean | false | Several lines; return inserts a line break unless submitBehavior says otherwise. |
maxLength | number | The most characters the user can enter. | |
editable | boolean | true | The user can change the text. |
secureTextEntry | boolean | false | The text is hidden (passwords). |
allowFontScaling | boolean | true | Follow the system text size. |
maxFontSizeMultiplier | number | 0 | The upper bound of the system scale (0 is none). |
#Keyboard
| Prop | Type | Default | Description |
|---|---|---|---|
keyboardType | "default" | "number-pad" | "decimal-pad" | "numeric" | "numbers-and-punctuation" | "email-address" | "phone-pad" | "url" | "default" | The keyboard layout (the values; numbers-and-punctuation has a minus sign on iOS). |
returnKeyType | "default" | "done" | "go" | "next" | "search" | "send" | "default" | The label of the return key. |
autoCapitalize | "none" | "sentences" | "words" | "characters" | "sentences" | Automatic capitalization. |
autoCorrect | boolean | true | The system’s corrections. |
autoComplete | AutoComplete | TextContentType | What the input holds, for autofill: "username", "current-password", "new-password", "one-time-code", "email", "tel", "postal-code", "cc-number", "off", … | |
submitBehavior | "submit" | "blurAndSubmit" | "newline" | "blurAndSubmit" (single line), "newline" (multiline) | What return does: "submit" reports onSubmitEditing and keeps the focus, "blurAndSubmit" also unfocuses, "newline" inserts a line break (multiline only; a single line submits). A multiline input that submits still takes line breaks from Shift+Return or Option / Alt+Return on hardware keyboards. Wins over blurOnSubmit. |
blurOnSubmit | boolean | true | Return unfocuses a single-line input; false keeps the keyboard up (e.g. to send several messages). submitBehavior replaces it. |
autoFocus | boolean | false | Focused when it appears. |
#Caret and selection
| Prop | Type | Description |
|---|---|---|
caretHidden | boolean | No caret is drawn. |
selectionColor | ColorValue | The color of the caret and the selection. |
selection | { start: number; end?: number } | The controlled selection, in UTF-16 units; end defaults to start (a caret). |
#iOS
ios key | Type | Description |
|---|---|---|
textContentType | AutoComplete | TextContentType | iOS’s names for autoComplete ("oneTimeCode", "newPassword", …). |
inputAccessoryViewID | string | The nativeID of an InputAccessoryView (nsh/ios) shown over the keyboard while the input has focus. |
keyboardAppearance | "default" | "light" | "dark" | The keyboard’s look; "default" follows the system’s appearance. Android’s keyboards follow the system’s theme. |
<Input autoComplete="one-time-code" keyboardType="number-pad" ios={{ textContentType: "oneTimeCode" }} />
#Events
| Event | Payload | When |
|---|---|---|
onChangeText | (text: string) | The user changed the text. |
onSubmitEditing | { text } | The return key was pressed in an input that submits (single line, or submitBehavior "submit" / "blurAndSubmit"). |
onFocus / onBlur | none | The input gained / lost focus. |
onSelectionChange | { start, end } | The caret or selection moved (UTF-16 units). |
onKeyPress | { key } | Before the text changes: key is the typed text, "Backspace" or "Enter". |
onContentSizeChange | { width, height } | The size of the text changed (at the input’s width when multiline). |
A multiline input that grows with its text:
const height = signal(40);
<Input
multiline
onContentSizeChange={(e) => height.set(Math.min(120, Math.max(40, e.height + 16)))}
style={{ height: height(), paddingHorizontal: 12, paddingVertical: 8 }}
/>;
#Ref methods
| Method | Description |
|---|---|
focus() | Focuses the input and shows the keyboard. |
blur() | Unfocuses it. |
setSelection(start, end?) | Selects start..end (UTF-16 units); a caret without end. |
Moving focus to the next field on return:
let password!: YNode;
<>
<Input returnKeyType="next" onSubmitEditing={() => password.focus()} />
<Input ref={password} secureTextEntry autoComplete="current-password" />
</>;
Keyboard.dismiss() from nsh/ui/mobile hides the keyboard whichever input has it.
#Platform notes
- Inputs inside a
<Scroll>are kept clear of the keyboard natively. Elsewhere useKeyboardAvoidingView. disableddisables the input as well as applyingdisabled:classes.inputAccessoryViewIDhas no effect on Android, which has no keyboard accessory bar.