Forms

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

ComponentPurpose
FieldsetGroups related fields with a legend
FieldWraps a single input with label, helper text, and error text
InputSingle-line text input
TextareaMulti-line text input
SelectDropdown for choosing from a list
ComboboxSearchable dropdown with filtering
AutocompleteText input with type-ahead suggestions
RadioGroupRadio buttons for selecting one option
LabelStandalone 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.

<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>Password must be at least 8 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.

<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

<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

<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

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.

<Fieldset>
  <Field invalid required>
    <Field.Label showIndicator>Email</Field.Label>
    <Field.Input type='email' />
    <Field.ErrorText>Please enter a valid email address.</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.

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

<Fieldset>
  <Field invalid required>
    <Field.Label showIndicator>Email</Field.Label>
    <Field.Input type='email' />
    <Field.ErrorText>Please enter a valid email address.</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>Please select 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>Please select 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