Skip to content

Input

A single-line, controlled text input widget with cursor navigation and editing support.

Usage

ui.input({
  id: "name",
  value: state.name,
  onInput: (value) => app.update((s) => ({ ...s, name: value })),
})

Props

Prop Type Default Description
id string required Unique identifier for focus and event routing
value string required Current input value (controlled)
accessibleLabel string - Optional semantic label for focus announcements and debugging
disabled boolean false Disable editing and dim appearance
readOnly boolean false Keep the input focusable/selectable while preventing edits
focusable boolean true Opt out of Tab focus order while keeping id-based routing available
style TextStyle - Custom styling (merged with focus/disabled state)
onInput (value: string, cursor: number) => void - Callback when value changes
onBlur () => void - Callback when input loses focus
focusConfig FocusConfig - Control focus visuals; { indicator: "none" } suppresses focused input decoration
key string - Reconciliation key for dynamic lists

Design System Styling

Inputs are design-system styled by default. The input renderer applies inputRecipe() output automatically from the active ThemeDefinition.

Prop Type Default Description
dsSize "sm" \| "md" \| "lg" "md" Size preset (controls padding and height)
placeholder string - Placeholder text shown when value is empty

Manual style overrides are merged on top of recipe output via mergeTextStyle(baseStyle, ownStyle) (they do not disable recipes).

Framed chrome requires both width >= 3 and height >= 3. At height = 1, the recipe still applies text/background styling, but no box border is drawn.

Behavior

Enabled inputs are focusable by default. readOnly keeps the input focusable/selectable while blocking edits, and focusable: false removes it from Tab traversal. Clicking the input focuses it when it remains focusable. When focused:

  • Text entry inserts at cursor position
  • Left/Right move by grapheme cluster
  • Ctrl+Left/Ctrl+Right move by word boundaries
  • Home/End move to start/end of input
  • Shift+Left/Shift+Right extends selection by grapheme
  • Shift+Home/Shift+End extends selection to start/end
  • Shift+Ctrl+Left/Shift+Ctrl+Right extends selection by word
  • Ctrl+A selects all text
  • Ctrl+C copies active selection to system clipboard (OSC 52)
  • Ctrl+X cuts active selection to system clipboard (OSC 52)
  • Ctrl+Z undoes the last edit
  • Ctrl+Shift+Z or Ctrl+Y redoes the last undone edit
  • Backspace/Delete remove one grapheme cluster when no selection is active
  • Backspace/Delete delete the selected range when selection is active
  • Typing with an active selection replaces the selected range
  • Paste strips \r/\n (single-line input) and keeps tabs
  • Tab moves focus to next widget

Inputs are always controlled - the value prop determines what is displayed, and onInput is how edits persist across frames. For multi-line text, use ui.textarea.

Input Editor State

Input editing is grapheme-aware and internally tracks:

  • cursor: current caret offset at a grapheme boundary
  • selectionStart: anchor offset, or null when no selection is active
  • selectionEnd: active/caret offset, or null when no selection is active

Renderer integrations receive these through the runtime input editor result to support selection highlighting.

Examples

Controlled input

type State = { email: string };

app.view((state) =>
  ui.input({
    id: "email",
    value: state.email,
    onInput: (value) => app.update((s) => ({ ...s, email: value })),
  })
);

With useForm binding

ui.input({ id: "email", ...form.bind("email") });

Validation on blur

Use onBlur to trigger validation when the user leaves the field:

ui.input({
  id: "email",
  value: state.email,
  onInput: (value) => app.update((s) => ({ ...s, email: value })),
  onBlur: () => validateEmail(state.email),
})

With a field wrapper

Combine with field for labels and error display:

ui.field({
  label: "Email",
  required: true,
  error: state.errors.email,
  children: ui.input({
    id: "email",
    value: state.email,
    onInput: (v) => app.update((s) => ({ ...s, email: v })),
  }),
})

Unicode Handling

Text editing is based on grapheme clusters using a pinned Unicode version. This ensures:

  • Emoji and combined characters are handled as single units
  • Cursor movement is consistent across platforms
  • Deterministic behavior for any input string