TextInput

TextInput is the native text entry primitive used by Zynth. It supports controlled and uncontrolled usage, native selection updates, secure entry, multiline input, and synchronous UI-thread handlers for custom text mutation.

Basic Usage

For controlled inputs, always use createSyncSignal from @zynthjs/core. Standard Solid signals (createSignal) should be avoided for the value prop as they can cause cursor jumps and synchronization lag.

import { createSyncSignal } from "@zynthjs/core";
import { TextInput, View, Text } from "@zynthjs/components";

function UsernameField() {
  // Use createSyncSignal for all controlled inputs
  const [value, setValue] = createSyncSignal("");

  return (
    <View>
      <TextInput value={value} onChangeText={setValue} placeholder="Username" />
      <Text>Current: {value()}</Text>
    </View>
  );
}

Controlled And Uncontrolled

TextInput can be driven by a Solid signal through value, or initialized once with defaultValue.

<TextInput value={value} onChangeText={setValue} />
<TextInput defaultValue="Hello" />

Use value when the parent should own the source of truth. Use defaultValue when the native field should manage its own state after mount.

Reactivity and createSyncSignal

For controlled TextInput usage, it is highly recommended to use createSyncSignal instead of a standard createSignal.

Standard signals can cause undesirable effects such as cursor jumps, text flickers, or synchronization lag because they rely on asynchronous bridge communication. createSyncSignal is specifically designed for responsive synchronization between the JavaScript environment and the native text buffer, ensuring that the field remains responsive and stable during rapid editing.

import { createSyncSignal } from "@zynthjs/core";
import { TextInput } from "@zynthjs/components";

function ControlledInput() {
  // Use createSyncSignal for controlled inputs to ensure stability
  const [text, setText] = createSyncSignal("");

  return (
    <TextInput value={text} onChangeText={setText} placeholder="Type here..." />
  );
}

Input Handlers (Worklets)

For text transformations, sanitization, or masking, use the handler prop combined with createInputHandler.

Because createInputHandler creates a UI-thread worklet, the transformation happens synchronously on the native side before the text is even committed to the buffer. This prevents the “jumping” effect often seen when using JavaScript-side onChangeText for formatting.

import { createInputHandler, createSyncSignal } from "@zynthjs/core";
import { TextInput } from "@zynthjs/components";

function CreditCardInput() {
  const [text, setText] = createSyncSignal("");

  // Transformation happens on the native UI thread
  const formatCard = createInputHandler((currentText, newChar, proposedText) => {
    const combined = proposedText.replace(/\D/g, "");
    if (combined.length > 16) return currentText;
    return combined.replace(/(.{4})/g, "$1 ").trim();
  });

  return (
    <TextInput
      value={text}
      onChangeText={setText}
      handler={formatCard}
      placeholder="XXXX XXXX XXXX XXXX"
    />
  );
}

Props

PropTypeDescription
valuestring | SyncSignalAccessor<string>Controlled text value. Prefer createSyncSignal for stability.
defaultValuestringInitial uncontrolled text value.
placeholderstringPlaceholder text shown when empty.
multilinebooleanEnables multiline entry.
numberOfLinesnumberHint for multiline height (rows).
maxLengthnumberNative maximum text length.
editablebooleanEnables or disables editing.
secureTextEntrybooleanMasks text entry like a password field.
inputModetext | numeric | decimal | tel | email | url | searchKeyboard/input mode hint.
autoCapitalizenone | sentences | words | charactersCapitalization behavior.
autoCorrectbooleanEnables native autocorrect.
spellCheckbooleanEnables native spell checking.
returnKeyTypedefault | go | next | search | send | doneReturn key label.
blurOnSubmitbooleanBlurs the field after submit.
submitBehaviornewline | submit | noneSubmission behavior for multiline fields.
selection{ start: number; end: number }Programmatic selection range.
selectionColorstringSelection highlight color (Hex).
caretColorstringCaret (cursor) color (Hex).
placeholderTextColorstringPlaceholder text color (Hex).
clearButtonModenever | while-editing | unless-editing | always(iOS) Clear button visibility.
showClearAccessorybooleanShows a clear accessory on supported platforms.
handler(current, next, proposed) => string | undefinedWorklet for UI-thread input transformation or rejection. Return undefined to accept native input unchanged.
onChangeText(text: string) => voidCalled when text changes.
onChange(event: TextChangeEvent) => voidNative change event callback with delta details.
onSelectionChange(selection: Selection) => voidCalled when the caret or selection changes.
onContentSizeChange(width: number, height: number) => voidCalled when multiline content size changes.
onSubmitEditing(text: string) => voidCalled when the return key is pressed.
onKeyPress(event: KeyEvent) => voidCalled on native key press events.
onFocus() => voidCalled when the field gains focus.
onBlur() => voidCalled when the field loses focus.
onCompositionStart() => voidCalled when IME composition starts.
onCompositionEnd() => voidCalled when IME composition ends.
selectTextOnFocusbooleanAutomatically selects all text when focused.
eventThrottleMsnumberThrottles native event dispatch to JS.
allowProgrammaticJumpDuringEditbooleanAllows JS to overwrite the buffer while the user is typing.
styleStyleProp | Accessor<StyleProp>Component styles.
testIDstringTest identifier for automation.
debugSyncbooleanEnables console logging for synchronization events.

Imperative Ref

The ref exposes the TextInputRef interface for imperative control.

export type TextInputRef = {
  text: () => string;
  setText: (value: string) => void;
  selection: () => Selection;
  setSelection: (sel: Selection) => void;
  isEditing: () => boolean;
  isComposing: () => boolean;
  hasPendingSync: () => boolean;
  focus: () => void;
  blur: () => void;
  clear: () => void;
  insertAtCursor: (text: string) => void;
  replaceRange: (start: number, end: number, text: string) => void;
  commit: () => void;
  cancelPending: () => void;
};

Usage

let inputRef: (HostNode & TextInputRef) | null = null;

<TextInput ref={(node) => (inputRef = node)} />;

// Focus the input
inputRef?.focus();

// Insert text at current cursor position
inputRef?.insertAtCursor("Hello");

// Clear text
inputRef?.clear();