# Forms

> Assembling forms from Field, Fieldset and Roadie controls: labels, required and invalid states, helper and error text.

Roadie provides a set of composable form components that handle accessibility,
validation states, and styling automatically. This page covers the patterns and
best practices for assembling them into forms.

## Component hierarchy

| Component                                                                                 | Purpose                                                       |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| [Fieldset](https://ticketsolutionsptyltd.github.io/roadie/components/fieldset.md)         | Groups related fields with a legend                           |
| [Field](https://ticketsolutionsptyltd.github.io/roadie/components/field.md)               | Wraps a single input with label, helper text, and error text  |
| [Input](https://ticketsolutionsptyltd.github.io/roadie/components/input.md)               | Single-line text input                                        |
| [Textarea](https://ticketsolutionsptyltd.github.io/roadie/components/textarea.md)         | Multi-line text input                                         |
| [Select](https://ticketsolutionsptyltd.github.io/roadie/components/select.md)             | Dropdown for choosing from a list                             |
| [Combobox](https://ticketsolutionsptyltd.github.io/roadie/components/combobox.md)         | Searchable dropdown with filtering                            |
| [Autocomplete](https://ticketsolutionsptyltd.github.io/roadie/components/autocomplete.md) | Text input with type-ahead suggestions                        |
| [RadioGroup](https://ticketsolutionsptyltd.github.io/roadie/components/radio-group.md)    | Radio buttons for selecting one option                        |
| [Checkbox](https://ticketsolutionsptyltd.github.io/roadie/components/checkbox.md)         | A single opt-in, or a group for picking any number of options |
| [Label](https://ticketsolutionsptyltd.github.io/roadie/components/label.md)               | Standalone styled label                                       |

## Context-driven state

Field, Select, and RadioGroup all use React context to flow state to their
children. Set `invalid`, `required`, and `disabled` on the root. Child inputs
pick up the corresponding aria attributes automatically.

```tsx
<Fieldset>
  <Field required>
    <Field.Label showIndicator>Email</Field.Label>
    <Field.Input type='email' placeholder='you@example.com' />
  </Field>
  <Field invalid>
    <Field.Label>Password</Field.Label>
    <Field.Input type='password' defaultValue='short' />
    <Field.ErrorText>Use at least eight characters.</Field.ErrorText>
  </Field>
  <Field disabled>
    <Field.Label>Username</Field.Label>
    <Field.Input defaultValue='taken-name' />
    <Field.HelperText>This field cannot be changed.</Field.HelperText>
  </Field>
</Fieldset>
```

## Indicators

Use `showIndicator` on any label component (`Field.Label`, `Select.Label`,
`RadioGroup.Label`) to automatically render a required or optional indicator
based on the parent's `required` prop. This eliminates the need to manually
import and place `RequiredIndicator` or `OptionalIndicator`.

```tsx
<Fieldset>
  <Field required>
    <Field.Label showIndicator>Full name</Field.Label>
    <Field.Input autoComplete='name' />
  </Field>
  <Field>
    <Field.Label showIndicator>Phone number</Field.Label>
    <Field.Input type='tel' />
  </Field>
</Fieldset>
```

## Field as universal wrapper

Field wraps any form control, not just Input and Textarea. Use it with Select,
RadioGroup, Combobox, and Autocomplete for consistent layout, labels, and error
handling. Field provides `grid gap-1.5` spacing, label wiring, and
`invalid`/`required`/`disabled` context that child controls inherit automatically.

### Select in Field

```tsx
<Field required>
  <Field.Label showIndicator>Industry</Field.Label>
  <Select defaultValue='music'>
    <Select.Trigger>
      <Select.Value placeholder='Select...' />
      <Select.Icon />
    </Select.Trigger>
    <Select.Content>
      <Select.Item value='music'>Music</Select.Item>
      <Select.Item value='sport'>Sport</Select.Item>
      <Select.Item value='arts'>Arts</Select.Item>
    </Select.Content>
  </Select>
</Field>
```

### RadioGroup in Field

```tsx
<Field required>
  <Field.Label showIndicator>Preferred contact method</Field.Label>
  <RadioGroup>
    <RadioGroup.Item value='email' label='Email' />
    <RadioGroup.Item value='phone' label='Phone' />
  </RadioGroup>
</Field>
```

### Combobox in Field

```tsx
function ComboboxInField() {
  const bands = [
    'Powderfinger',
    'Custard',
    'Regurgitator',
    'Violent Soho',
    'DZ Deathrays',
  ]

  return (
    <Field required>
      <Field.Label showIndicator>Favourite band</Field.Label>
      <Combobox items={bands}>
        <Combobox.InputGroup>
          <Combobox.Input placeholder='Search bands...' />
          <Combobox.Clear />
          <Combobox.Trigger />
        </Combobox.InputGroup>
        <Combobox.Portal>
          <Combobox.Positioner>
            <Combobox.Popup>
              <Combobox.List>
                {(band) => (
                  <Combobox.Item key={band} value={band}>
                    {band}
                    <Combobox.ItemIndicator />
                  </Combobox.Item>
                )}
              </Combobox.List>
              <Combobox.Empty>No bands found</Combobox.Empty>
            </Combobox.Popup>
          </Combobox.Positioner>
        </Combobox.Portal>
      </Combobox>
    </Field>
  )
}
```

## Select shortcuts

Use `Select.Content` instead of manually nesting Portal, Positioner, and Popup.
Pass string children to `Select.Item` for automatic `ItemText` and
`ItemIndicator` wrapping. The individual primitives remain available for
advanced customisation.

## Error handling

`Field.ErrorText` only renders when the parent Field has `invalid` set.
`Select.ErrorText` and `RadioGroup.ErrorText` hide when `invalid` is explicitly
`false`. This means you can always include the error text in your markup without
conditional wrappers. Word the message with
[Interface copy](https://ticketsolutionsptyltd.github.io/roadie/content/interface-copy.md#errors).

```tsx
<Fieldset>
  <Field invalid required>
    <Field.Label showIndicator>Email</Field.Label>
    <Field.Input type='email' />
    <Field.ErrorText>Enter an email address like name@example.com.</Field.ErrorText>
  </Field>
  <Field required>
    <Field.Label showIndicator>Name</Field.Label>
    <Field.Input defaultValue='Luke' />
    <Field.ErrorText>This error is hidden because the field is valid.</Field.ErrorText>
  </Field>
</Fieldset>
```

## Grouping fields

Use `Fieldset` with `Fieldset.Legend` to group related fields. This provides
semantic HTML grouping and a visible legend for screen readers.

```tsx
<Fieldset>
  <Fieldset.Legend>
    <h3 className='text-display-ui-5 text-strong'>Contact details</h3>
  </Fieldset.Legend>
  <Field required>
    <Field.Label showIndicator>Full name</Field.Label>
    <Field.Input autoComplete='name' />
  </Field>
  <Field required>
    <Field.Label showIndicator>Email</Field.Label>
    <Field.Input type='email' autoComplete='email' />
  </Field>
  <Field>
    <Field.Label showIndicator>Phone</Field.Label>
    <Field.Input type='tel' autoComplete='tel' />
  </Field>
</Fieldset>
```

## Validation with Field

Set `invalid` on the Field root to show error states across any control.
Field.ErrorText auto-hides when invalid is falsy.

```tsx
<Fieldset>
  <Field invalid required>
    <Field.Label showIndicator>Email</Field.Label>
    <Field.Input type='email' />
    <Field.ErrorText>Enter an email address like name@example.com.</Field.ErrorText>
  </Field>
  <Field invalid required>
    <Field.Label showIndicator>Industry</Field.Label>
    <Select>
      <Select.Trigger>
        <Select.Value placeholder='Select...' />
        <Select.Icon />
      </Select.Trigger>
      <Select.Content>
        <Select.Item value='music'>Music</Select.Item>
        <Select.Item value='sport'>Sport</Select.Item>
      </Select.Content>
    </Select>
    <Field.ErrorText>Choose an industry.</Field.ErrorText>
  </Field>
  <Field invalid required>
    <Field.Label showIndicator>Contact method</Field.Label>
    <RadioGroup>
      <RadioGroup.Item value='email' label='Email' />
      <RadioGroup.Item value='phone' label='Phone' />
    </RadioGroup>
    <Field.ErrorText>Choose a contact method.</Field.ErrorText>
  </Field>
</Fieldset>
```

## Guidelines

### Do

- Wrap all form controls in `Field` for consistent layout, labels, and error handling.
- Set `invalid`, `required`, and `disabled` on the `Field` root. Child controls inherit these automatically.
- Use `Field.Label` with `showIndicator` for all form control labels.
- Use `Field.ErrorText` and `Field.HelperText` for feedback text.
- Use `Select.Content` for standard dropdowns.
- Use `Fieldset` with `Fieldset.Legend` to group related fields.

### Don't

- Don't pass `aria-invalid` manually. Field context handles it.
- Don't import `RequiredIndicator` or `OptionalIndicator` directly when using `showIndicator`.
- Don't add manual `<div className='grid gap-1.5'>` wrappers. Field provides spacing.
- Don't use `Select.Portal` + `Select.Positioner` + `Select.Popup` when `Select.Content` suffices.
- Don't wrap `Field.ErrorText` in conditional rendering (`{error && ...}`). The component handles visibility.
- Don't set a default `intent` on form components. Intent flows via the CSS cascade.

Docs updated 15 Oct 2026.
