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 boundaryselectionStart: anchor offset, ornullwhen no selection is activeselectionEnd: active/caret offset, ornullwhen 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¶
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