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.

el-input.tsx Edit in Playground Open in new tab
import { Input, mount, signal, Text, View, type YNode } from "nsh";

function App() {
  const name = signal("");
  const password = signal("");
  const notes = signal("");
  const submitted = signal("");
  let passwordInput!: YNode;
  const field = {
    fontSize: 16,
    color: "#1f2328",
    backgroundColor: "#ffffff",
    borderWidth: 1,
    borderColor: "#d0d7de",
    borderRadius: 10,
    paddingHorizontal: 12,
    paddingVertical: 10,
  } as const;
  return (
    <View style={{ flex: 1, backgroundColor: "#f6f8fa", padding: 20, paddingTop: 24, gap: 10 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: "#1f2328" }}>input</Text>

      <Text style={{ color: "#57606a" }}>Name (controlled, maxLength 20)</Text>
      <Input
        style={field}
        value={name()}
        onChangeText={(text) => name.set(text)}
        placeholder="Ada Lovelace"
        placeholderTextColor="#8c959f"
        autoCapitalize="words"
        maxLength={20}
        returnKeyType="next"
        onSubmitEditing={() => passwordInput.focus()}
      />
      <Text style={{ fontSize: 12, color: "#6e7781" }}>{name().length} / 20</Text>

      <Text style={{ color: "#57606a" }}>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: "#57606a" }}>Notes (multiline)</Text>
      <Input
        style={{ ...field, height: 100 }}
        multiline
        value={notes()}
        onChangeText={(text) => notes.set(text)}
        placeholder="Anything else?"
        placeholderTextColor="#8c959f"
      />

      <View
        onPress={() => submitted.set(`Signed in as ${name() || "nobody"}`)}
        activeOpacity={0.7}
        disabled={password().length === 0}
        class="disabled:opacity-40"
        style={{ backgroundColor: "#1f883d", borderRadius: 10, padding: 14, alignItems: "center", marginTop: 6 }}
      >
        <Text style={{ color: "#ffffff", fontSize: 16, fontWeight: "600" }}>Sign in</Text>
      </View>
      <Text style={{ color: "#1a7f37" }}>{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 value and update it in onChangeText. JS can rewrite what the user typed (trim it, format it): the text shows the edit at once and is corrected when the new value arrives. Each native edit carries a count, and a value from 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 from onChangeText or onSubmitEditing.

Rules that must hold while the user types are props, answered natively without asking JS: maxLength limits the length and 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

PropTypeDefaultDescription
valuestringThe text, controlled by JS.
defaultValuestringThe initial text.
placeholderstringShown while the input is empty.
placeholderTextColorColorValueThe placeholder’s color.
multilinebooleanfalseSeveral lines; return inserts a line break.
maxLengthnumberThe most characters the user can enter.
editablebooleantrueThe user can change the text.
secureTextEntrybooleanfalseThe text is hidden (passwords).
allowFontScalingbooleantrueFollow the system text size.
maxFontSizeMultipliernumber0The upper bound of the system scale (0 is none).

#Keyboard

PropTypeDefaultDescription
keyboardType"default" | "number-pad" | "numeric" | "decimal-pad" | "email-address" | "phone-pad" | "url""default"The keyboard layout.
returnKeyType"default" | "done" | "go" | "next" | "search" | "send""default"The label of the return key.
autoCapitalize"none" | "sentences" | "words" | "characters""sentences"Automatic capitalization.
autoCorrectbooleantrueThe system’s corrections.
autoCompleteAutoComplete | TextContentTypeWhat the input holds, for autofill: "username", "current-password", "new-password", "one-time-code", "email", "tel", "postal-code", "cc-number", "off", …
blurOnSubmitbooleantrueReturn unfocuses a single-line input; false keeps the keyboard up (e.g. to send several messages).
autoFocusbooleanfalseFocused when it appears.

#Caret and selection

PropTypeDescription
caretHiddenbooleanNo caret is drawn.
selectionColorColorValueThe 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 keyTypeDescription
textContentTypeAutoComplete | TextContentTypeiOS’s names for autoComplete ("oneTimeCode", "newPassword", …).
inputAccessoryViewIDstringThe nativeID of an InputAccessoryView (nsh/ios) shown over the keyboard while the input has focus.
<Input autoComplete="one-time-code" keyboardType="number-pad" ios={{ textContentType: "oneTimeCode" }} />

#Events

EventPayloadWhen
onChangeText(text: string)The user changed the text.
onSubmitEditing{ text }The return key was pressed (single line).
onFocus / onBlurnoneThe 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

MethodDescription
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 use KeyboardAvoidingView.
  • disabled disables the input as well as applying disabled: classes.
  • inputAccessoryViewID has no effect on Android, which has no keyboard accessory bar.