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. 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. 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 native radio or checkbox inputs. The complete example uses ToggleGroup.Item with a shared name, checked state, and per-item onChange handlers to own selection explicitly. 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 provides initial server markup, renders during client hydration, and updates whenever model.set changes query.

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

TSXCreate: src/components/ProjectFilter.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.

TSXCreate: src/components/EmailField.tsx
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. 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 (January is 0). Pass an explicit month and year for predictable server output.

  • Calendar shows previous/next buttons and month/year selectors when onMonthChange is supplied. Update year and month in that callback, and update selected in onSelect. yearRange controls the year options; min and max disable out-of-range dates.

  • 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. inputLabel names its nested native date input; Field wiring targets the DatePicker wrapper, so it does not replace inputLabel.

  • DatePicker does not expose Calendar onMonthChange or yearRange. Use a separately controlled Calendar when you need custom month navigation. Control DatePicker open with your application and keep calendar markup present before opening it.

  • Combobox is a native input plus datalist. listId defaults 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.

  • NumberInput reports valid drafts through onValueInput(value, draft). onValueChange clamps a committed number to min/max. Empty input becomes null when allowUnset is true; invalid intermediate text does not emit a numeric value. Arrow keys step the value and Shift multiplies the step by ten.

  • ColorPicker defaults to format=hex. format=css adds text entry for CSS colors, gradients, and variables, with optional swatches and tokens. onValueInput previews and onValueChange commits; this broader CSS format does not change the theme config requirement for hex brand anchors.

  • ObjectField requires label and a JSON object value. onDraft includes validation information, onCommit is called only for a valid object, and onValidationChange lets the application show problems. Read validation.valid before storing a draft.

  • FileTrigger opens a native file picker and reports a FileList through onFilesChange. 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.

  • TreeView supports controlled or initial selection and expansion independently. Use stable item IDs; onActivate performs an application action, while onDrop reports a requested move that the application must validate and apply to its tree data.