Forms and component state
Build labelled forms and understand controlled, initial, native, validation, search, and selection behavior.
Choose controlled, initial, and native state intentionally
Tavo.js UI does not impose one state model on every control. Native controls accept their normal checked, defaultChecked, value, or defaultValue inputs. Higher-level components such as Tabs, Calendar, DatePicker, SearchInput clearing, and ToggleGroup render from values supplied by the application.
Checkbox, Radio, and Switch preserve native change events and form submission.
Tabs requires tabs and
activeId.onChangereports the requested ID; the application must store it and rerender.If Tabs receives an unknown
activeId, it shows the first item. An empty tabs array renders no markup.ToggleGroupuses checked native radio or checkbox inputs. Single mode defaults to a radiogroup; multiple mode defaults to a group.SearchInputcan renderdefaultValue, but its custom clear button is based on the controlled value prop.
Own interactive state in a Tavo.js component
A controlled component renders the value that your application supplies and reports proposed changes through a callback. Keep that value in a createTavo model when the current choice must be visible to sibling UI or reset by another control.
This example is complete: typing updates the model, the message rerenders from the same value, and Clear resets both the input and the message.
This model is local to each mounted
ProjectFilterinstance; it is not shared across requests or pages.The view runs in the browser after hydration and again whenever
model.setchanges query.Use a shared store only when two separately mounted consumers must observe the same value.
tsximport { createTavo } from "@tavojs/core";
import { Button, SearchInput, Stack, Text } from "@tavojs/ui";
type FilterState = { query: string };
export const ProjectFilter = createTavo<Record<string, never>, FilterState>({
model: () => ({ query: "" }),
view: ({ state, model }) => (
<Stack gap="sm">
<SearchInput
aria-label="Filter projects"
placeholder="Filter projects"
value={state.query}
clearable
onInput={(event) => model.set("query", event.currentTarget.value)}
onClear={() => model.set("query", "")}
/>
<Text>
{state.query ? `Filtering for “${state.query}”` : "Showing all projects"}
</Text>
<Button
type="button"
variant="text"
onClick={() => model.set("query", "")}
>
Reset filter
</Button>
</Stack>
)
});Let Field wire one control
Field renders a visible label, annotates the first child control, and renders at most one message. It preserves an explicit control ID; otherwise it uses the Field id or derives one from a string label.
Message priority is error, warning, success, then hint.
The message ID is appended to an existing
aria-describedbyvalue.An error sets
aria-invalid=true on the first control.required shows a visual required marker; optional shows Optional. They do not replace the control's native required prop.
Only the first child control receives generated ID and description wiring. Use separate Field components for separate controls.
Repeated labels can derive repeated IDs. Supply stable unique id values in repeated rows and forms.
tsximport { Field, TextInput } from "@tavojs/ui";
export const EmailField = ({ value, error, onInput }) => {
return (
<Field
id="account-email"
label="Email"
hint="Used for receipts"
error={error}
required
>
<TextInput type="email" value={value} required onInput={onInput} />
</Field>
);
};Form and selection defaults
Checkbox: indeterminate=false, size=md, tone=primary. Indeterminate adds
aria-checked=mixed but remains application-controlled.Radio: size=md and tone=primary.
RadioGroupdefaults to vertical and supplies its name to descendant Radio components that do not already have one.Switch: size=md and tone=primary. It is a native checkbox styled as a switch.
Select and
TextInput: size=md. Textarea defaults to rows=4 and resize=vertical.Slider: min=0, max=100, step=1.
Toggle: pressed=false, size=md, variant=default.
ToggleGroupdefaults to type=single, size=md, and variant=default.FormControlrenders a form and defaultsfullWidthto false.FormControlLabeldefaults to size=md.
Search, calendar, and date behavior
SearchInputalways renders a native search control. size defaults to md, clearable and loading default to false, andclearLabeldefaults to Clear search.The clear action appears only when value is a non-empty controlled string, clearable is true,
onClearexists, and the input is neither disabled nor read-only.Calendar defaults locale to en-US. selected, min, and max use YYYY-MM-DD values; month is zero-based because it follows the
JavaScriptDate month contract.DatePickerdefaults size to md, open to false, andrenderCalendarWhenClosedto true. Set the last option to false when closed calendar markup is unnecessary.DatePickerreports native date changes and calendar selections throughonValueChange.Combobox is a native input plus datalist.
listIddefaults from the name, or to combobox-options when neither is supplied.