Overview
The input widget creates a focusable, editable text input field. It uses controlled component pattern where the value is managed by your application state.
Basic Usage
import { ui } from "@rezi-ui/core";
// Simple input
ui.input("name-input", state.name)
// Input with change handler
ui.input("name-input", state.name, {
onInput: (value) => app.update({ name: value })
})
// Full props object
ui.input({
id: "email-input",
value: state.email,
placeholder: "Enter your email",
onInput: handleEmailChange,
onBlur: validateEmail
})
Props
Unique identifier for focus routing. Required for all inputs.
Current input value. Must be controlled by your application state.
onInput
(value: string, cursor: number) => void
Callback invoked when the input value changes. Receives the new value and cursor position.
Callback invoked when the input loses focus.
Placeholder text displayed when value is empty.
When true, input cannot be focused or edited.
When false, opt out of Tab focus order while keeping id-based routing available.
Optional semantic label for accessibility and debug announcements.
Styling Props
Optional style applied to the input value (merged with focus/disabled state).
Design system size preset: "xs", "sm", "md", "lg", "xl".Affects input height and padding when recipe styling is active.
Optional focus appearance configuration for custom focus indicators.
Internal Props
Internal prop used by ui.textarea(). Do not set directly.
Visible line count when multiline mode is enabled. Used by ui.textarea().
Wrap long lines in multiline mode. Used by ui.textarea().
Event Handling
The onInput callback receives both the new value and cursor position:
ui.input({
id: "username",
value: state.username,
onInput: (value, cursor) => {
// Update state with new value
app.update({ username: value });
// Cursor position is available if needed
console.log(`Cursor at position ${cursor}`);
}
})
Blur Events
Use onBlur to trigger validation when the input loses focus:
ui.input({
id: "email",
value: state.email,
onInput: (value) => app.update({ email: value }),
onBlur: () => {
// Validate when user leaves the field
const error = validateEmail(state.email);
app.update({ emailError: error });
}
})
Design System Integration
When the active theme provides semantic color tokens, inputs automatically use design system recipes with:
- Border styling
- Elevated background
- Focus state colors
- Size-based padding
Note: A framed border requires at least 3 rows of height. At 1 row, recipe text/background styling is still applied but without a box border.
// Standard input (uses recipe if theme supports it)
ui.input("name", state.name)
// Large input
ui.input("title", state.title, { dsSize: "lg" })
// Small input
ui.input("code", state.code, { dsSize: "sm" })
If the active theme does not provide semantic tokens, inputs fall back to non-recipe rendering.
Inputs are typically wrapped with ui.field() for labels and error messages:
ui.field({
label: "Email Address",
required: true,
error: state.errors.email,
hint: "We'll never share your email",
children: ui.input({
id: "email",
value: state.email,
placeholder: "[email protected]",
onInput: (value) => app.update({ email: value }),
onBlur: () => validateField("email")
})
})
Validation States
Error State
Show validation errors using the ui.field() wrapper:
ui.field({
label: "Username",
error: state.touched.username ? state.errors.username : undefined,
children: ui.input({
id: "username",
value: state.username,
onInput: (value) => {
app.update({ username: value });
},
onBlur: () => {
app.update({ touched: { ...state.touched, username: true } });
}
})
})
Disabled State
ui.input({
id: "readonly-field",
value: state.computedValue,
disabled: true
})
Controlled Pattern
Inputs use a controlled component pattern. You must manage the value in your application state:
// Define state
type State = {
username: string;
};
const initialState: State = {
username: ""
};
// In your view function
function view(state: State) {
return ui.input({
id: "username",
value: state.username, // Value from state
onInput: (value) => {
app.update({ username: value }); // Update state on change
}
});
}
Examples
ui.form([
ui.field({
label: "Email",
error: state.errors.email,
children: ui.input({
id: "email",
value: state.email,
placeholder: "[email protected]",
onInput: (value) => app.update({ email: value }),
onBlur: () => validateEmail()
})
}),
ui.field({
label: "Password",
error: state.errors.password,
children: ui.input({
id: "password",
value: state.password,
placeholder: "Enter password",
onInput: (value) => app.update({ password: value })
})
}),
ui.actions([
ui.button("login", "Log In", {
intent: "primary",
disabled: !state.isValid
})
])
])
ui.row({ gap: 1, items: "center" }, [
ui.icon("ui.search"),
ui.input({
id: "search",
value: state.query,
placeholder: "Search...",
onInput: (value) => {
app.update({ query: value });
// Debounce and trigger search
scheduleSearch(value);
}
})
])
Inline Edit
state.editing
? ui.input({
id: "title",
value: state.title,
onInput: (value) => app.update({ title: value }),
onBlur: () => {
saveTitle(state.title);
app.update({ editing: false });
}
})
: ui.text(state.title)
ui.input({
id: "age",
value: String(state.age),
onInput: (value) => {
const num = Number.parseInt(value, 10);
if (!Number.isNaN(num) && num >= 0) {
app.update({ age: num });
}
}
})
Keyboard Shortcuts
When focused, inputs support:
- Arrow Left/Right - Move cursor
- Home/End - Jump to start/end
- Backspace/Delete - Delete characters
- Ctrl+A (or Cmd+A) - Select all
- Ctrl+C/V/X (or Cmd+C/V/X) - Copy/paste/cut
- Enter - Submit (if parent form handles it)
- Escape - Clear selection or blur
Accessibility
- Inputs require a unique
id for focus routing
- Use
accessibleLabel for descriptive announcements
- Wrap with
ui.field() to associate labels with inputs
- Disabled inputs are not focusable or editable
- Focus indicators are shown when navigating with keyboard
- Textarea - For multi-line text input
- Field - For wrapping inputs with labels and errors
- Button - For form submission
- Select - For dropdown selection
- Checkbox - For boolean inputs