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. Tabs and Calendar use application state. ToggleGroup, TreeView, and native disclosures also expose initial-value props when the browser can own the interaction.
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 native radio or checkbox inputs. The complete example usesToggleGroup.Itemwith a shared name, checked state, and per-itemonChangehandlers to own selection explicitly. 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 provides initial server markup, renders during client hydration, and updates 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";
type Props = {
value: string;
error?: string;
onInput: (event: Event & { currentTarget: HTMLInputElement }) => void;
};
export const EmailField = ({ value, error, onInput }: Props) => {
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 (January is 0). Pass an explicit month and year for predictable server output.
Calendar shows previous/next buttons and month/year selectors when
onMonthChangeis supplied. Update year and month in that callback, and update selected inonSelect.yearRangecontrols the year options; min and max disable out-of-range dates.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.inputLabelnames its nested native date input; Field wiring targets theDatePickerwrapper, so it does not replaceinputLabel.DatePickerdoes not expose CalendaronMonthChangeoryearRange. Use a separately controlled Calendar when you need custom month navigation. ControlDatePickeropen with your application and keep calendar markup present before opening it.Combobox is a native input plus datalist.
listIddefaults from the name, or to combobox-options when neither is supplied.
Separate preview values from committed edits
Inspector-style controls report changes at different stages. Use preview callbacks to update local feedback and commit callbacks to save, create an undo entry, or start expensive work. These components do not persist values themselves.
NumberInputreports valid drafts throughonValueInput(value, draft).onValueChangeclamps a committed number to min/max. Empty input becomes null whenallowUnsetis true; invalid intermediate text does not emit a numeric value. Arrow keys step the value and Shift multiplies the step by ten.ColorPickerdefaults to format=hex. format=css adds text entry for CSS colors, gradients, and variables, with optional swatches and tokens.onValueInputpreviews andonValueChangecommits; this broader CSS format does not change the theme config requirement for hex brand anchors.ObjectFieldrequires label and a JSON object value.onDraftincludes validation information,onCommitis called only for a valid object, andonValidationChangelets the application show problems. Readvalidation.validbefore storing a draft.FileTriggeropens a native file picker and reports aFileListthroughonFilesChange. It does not read, parse, upload, or validate file contents. Copy the selected File objects during the callback before starting asynchronous work; the input resets after selection by default so the same file can be chosen again.TreeViewsupports controlled or initial selection and expansion independently. Use stable item IDs;onActivateperforms an application action, whileonDropreports a requested move that the application must validate and apply to its tree data.