Navigated to /docs/ui/forms-and-state

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. onChange reports 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.

  • ToggleGroup uses checked native radio or checkbox inputs. Single mode defaults to a radiogroup; multiple mode defaults to a group.

  • SearchInput can render defaultValue, 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 ProjectFilter instance; it is not shared across requests or pages.

  • The view runs in the browser after hydration and again whenever model.set changes query.

  • Use a shared store only when two separately mounted consumers must observe the same value.

TSX
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-describedby value.

  • 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.

TSX
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. RadioGroup defaults 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. ToggleGroup defaults to type=single, size=md, and variant=default.

  • FormControl renders a form and defaults fullWidth to false. FormControlLabel defaults to size=md.

Search, calendar, and date behavior

  • SearchInput always renders a native search control. size defaults to md, clearable and loading default to false, and clearLabel defaults to Clear search.

  • The clear action appears only when value is a non-empty controlled string, clearable is true, onClear exists, 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 JavaScript Date month contract.

  • DatePicker defaults size to md, open to false, and renderCalendarWhenClosed to true. Set the last option to false when closed calendar markup is unnecessary.

  • DatePicker reports native date changes and calendar selections through onValueChange.

  • Combobox is a native input plus datalist. listId defaults from the name, or to combobox-options when neither is supplied.