Overview
The field widget wraps form inputs with a label, optional error message, required indicator, and hint text. It provides consistent form field layout and associates labels with inputs for better accessibility.
Basic Usage
import { ui } from "@rezi-ui/core";
ui.field({
label: "Username",
children: ui.input({
id: "username",
value: state.username,
onInput: (value) => app.update({ username: value })
})
})
Props
Field label displayed above the input. Shown in bold by default.
The wrapped input widget (input, textarea, select, checkbox, etc.).
Error message to display below the input in red. Only shown when non-empty.
When true, displays an asterisk (*) after the label to indicate required field.
Help text displayed below the input in dimmed color. Shown below error if both exist.
Optional reconciliation key for the field wrapper.
Label with Required Indicator
ui.field({
label: "Email Address",
required: true,
children: ui.input({
id: "email",
value: state.email
})
})
// Displays: "Email Address *"
Error Messages
Error messages are displayed in red below the input:
ui.field({
label: "Password",
required: true,
error: state.errors.password,
children: ui.input({
id: "password",
value: state.password,
onInput: (value) => app.update({ password: value }),
onBlur: () => validatePassword()
})
})
Hint Text
Provide helpful guidance with hint text:
ui.field({
label: "Username",
hint: "Must be 3-20 characters, letters and numbers only",
children: ui.input({
id: "username",
value: state.username
})
})
Fields are typically used within ui.form() for consistent spacing:
ui.form([
ui.field({
label: "Email",
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: () => validateEmail()
})
}),
ui.field({
label: "Password",
required: true,
error: state.errors.password,
hint: "At least 8 characters",
children: ui.input({
id: "password",
value: state.password,
onInput: (value) => app.update({ password: value })
})
}),
ui.actions([
ui.button("submit", "Sign Up", {
intent: "primary",
disabled: !state.isValid
})
])
])
Validation Patterns
On Blur Validation
ui.field({
label: "Email",
required: true,
error: state.touched.email ? state.errors.email : undefined,
children: ui.input({
id: "email",
value: state.email,
onInput: (value) => app.update({ email: value }),
onBlur: () => {
app.update({ touched: { ...state.touched, email: true } });
const error = validateEmail(state.email);
app.update({ errors: { ...state.errors, email: error } });
}
})
})
On Submit Validation
ui.form([
ui.field({
label: "Username",
error: state.submitted ? state.errors.username : undefined,
children: ui.input({
id: "username",
value: state.username,
onInput: (value) => app.update({ username: value })
})
}),
ui.actions([
ui.button("submit", "Submit", {
intent: "primary",
onPress: () => {
const errors = validateForm(state);
app.update({ submitted: true, errors });
if (Object.keys(errors).length === 0) {
submitForm(state);
}
}
})
])
])
Real-Time Validation
ui.field({
label: "Username",
required: true,
error: state.username.length > 0 && state.username.length < 3
? "Username must be at least 3 characters"
: undefined,
hint: `${state.username.length}/20 characters`,
children: ui.input({
id: "username",
value: state.username,
onInput: (value) => {
if (value.length <= 20) {
app.update({ username: value });
}
}
})
})
Examples
Text Input Field
ui.field({
label: "Full Name",
required: true,
error: state.errors.name,
children: ui.input({
id: "name",
value: state.name,
placeholder: "John Doe",
onInput: (value) => app.update({ name: value })
})
})
Textarea Field
ui.field({
label: "Description",
required: true,
error: state.errors.description,
hint: "Provide a detailed description (min 20 characters)",
children: ui.textarea({
id: "description",
value: state.description,
rows: 5,
onInput: (value) => app.update({ description: value })
})
})
Select Field
ui.field({
label: "Country",
required: true,
error: state.errors.country,
children: ui.select({
id: "country",
value: state.country,
placeholder: "Select a country",
options: [
{ value: "us", label: "United States" },
{ value: "uk", label: "United Kingdom" },
{ value: "ca", label: "Canada" }
],
onChange: (value) => app.update({ country: value })
})
})
Checkbox Group Field
ui.field({
label: "Notification Preferences",
hint: "Choose how you want to be notified",
children: ui.column({ gap: 1 }, [
ui.checkbox({
id: "email-notif",
checked: state.emailNotifications,
label: "Email notifications",
onChange: (checked) => app.update({ emailNotifications: checked })
}),
ui.checkbox({
id: "push-notif",
checked: state.pushNotifications,
label: "Push notifications",
onChange: (checked) => app.update({ pushNotifications: checked })
}),
ui.checkbox({
id: "sms-notif",
checked: state.smsNotifications,
label: "SMS notifications",
onChange: (checked) => app.update({ smsNotifications: checked })
})
])
})
Radio Group Field
ui.field({
label: "Subscription Plan",
required: true,
error: state.errors.plan,
children: ui.radioGroup({
id: "plan",
value: state.plan,
options: [
{ value: "free", label: "Free - $0/month" },
{ value: "pro", label: "Pro - $10/month" },
{ value: "enterprise", label: "Enterprise - Contact Sales" }
],
onChange: (value) => app.update({ plan: value })
})
})
Slider Field
ui.field({
label: "Volume",
hint: "Adjust audio volume",
children: ui.slider({
id: "volume",
value: state.volume,
min: 0,
max: 100,
step: 5,
onChange: (value) => app.update({ volume: value })
})
})
Complex Validation
ui.field({
label: "Password",
required: true,
error: (() => {
if (!state.touched.password) return undefined;
if (state.password.length < 8) return "Must be at least 8 characters";
if (!/[A-Z]/.test(state.password)) return "Must contain an uppercase letter";
if (!/[0-9]/.test(state.password)) return "Must contain a number";
return undefined;
})(),
hint: "At least 8 characters with uppercase and number",
children: ui.input({
id: "password",
value: state.password,
onInput: (value) => app.update({ password: value }),
onBlur: () => app.update({ touched: { ...state.touched, password: true } })
})
})
Layout Structure
Fields render in a vertical column layout:
[Label] * ← Bold, with asterisk if required
[Input Widget] ← The wrapped children
[Error Message] ← Red, only if error is set
[Hint Text] ← Dimmed, always shown if set
All elements are part of a column with gap: 0 for tight spacing.
Styling
Fields use default styles:
- Label: Bold text
- Required indicator: Asterisk (*) appended to label
- Error: Red text (
fg: { r: 255, g: 0, b: 0 })
- Hint: Dimmed text
You cannot currently customize field wrapper styles, but you can style the wrapped input widget directly.
Accessibility
- Labels are visually associated with inputs
- Required fields are marked with asterisk
- Error messages provide validation feedback
- Hint text provides helpful guidance
- Use semantic
label text that clearly describes the field
Best Practices
- Always provide a label - Every input should have a descriptive label
- Use required indicator - Mark required fields with
required: true
- Show errors conditionally - Only show errors after user interaction or submission
- Provide helpful hints - Use hint text to guide users on format or requirements
- Keep labels concise - Use 1-3 words when possible
- Use sentence case - “Email address” not “Email Address”