Knit UI
GitHub

Forms

The field contract

Every input in the kit takes the same four presentation props, and wires them to the control with generated ids so the accessible name and description are correct:

<TextInput
  label="Email"
  description="We only use this for receipts."
  error={errors.email}
  required
/>

error is a node, not a boolean: pass the message and the field colours itself ($red9 border, red message) and announces. Pass undefined when valid.

The family

ComponentFor
TextInputsingle-line text
Textareamulti-line, with minRows/maxRows
PasswordInputtext with a reveal toggle
NumberInputnumeric, with steppers and clamping
MaskInputformatted input (phone, card)
PinInputone-time codes
Select · MultiSelectchoose from a list
Autocomplete · Comboboxfilter as you type
TagsInputfree-form chips
Checkbox · Radio · Switchbooleans and choices
Slider · Ratingranges
FileInput · FileButtonfiles
ColorInput · ColorPickercolours
DateInput · TimeInputdates and times

InputBase is the shared shell if you need to put a custom control inside a kit-shaped field.

Controlled and uncontrolled

Every input supports both, through the same useUncontrolled hook internally:

<TextInput defaultValue="hello" />                       // uncontrolled
<TextInput value={value} onChangeText={setValue} />      // controlled

Pass value or defaultValue, never both.

Layout

<Stack gap="$md">
  <TextInput label="Name" />
  <Group grow>
    <TextInput label="City" />
    <TextInput label="Postcode" />
  </Group>
  <Fieldset legend="Delivery">
    <Radio.Group value={speed} onChange={setSpeed}>
      <Radio value="standard" label="Standard" />
      <Radio value="express" label="Express" />
    </Radio.Group>
  </Fieldset>
  <Group justify="flex-end">
    <Button variant="subtle">Cancel</Button>
    <Button loading={saving}>Save</Button>
  </Group>
</Stack>

Fieldset gives you a real <fieldset>/<legend> on the web, which screen readers use to group the controls.

Validation

The kit does not ship a form library — use React Hook Form, TanStack Form, or your own state. What it gives you is the presentation contract above. With React Hook Form:

<Controller
  control={control}
  name="email"
  render={({ field, fieldState }) => (
    <TextInput
      label="Email"
      value={field.value}
      onChangeText={field.onChange}
      onBlur={field.onBlur}
      error={fieldState.error?.message}
    />
  )}
/>

useValidatedState covers the simple case without a library.

The keyboard on device

Wrap a form in KeyboardAvoidingView, or use KeyboardAwareScrollView when it scrolls — both are in the kit and use the platform Keyboard API.

Set the right keyboard and autofill hints — they cost nothing and improve every form:

<TextInput
  label="Email"
  keyboardType="email-address"
  autoCapitalize="none"
  autoComplete="email"
  textContentType="emailAddress"
/>

Submitting

Keep the button mounted while it works — loading blocks interaction without unmounting, so focus is never lost mid-request:

<Button loading={saving} onPress={submit}>Save</Button>