Skip to content

Form controls

The form primitives. Field owns the label/hint/error and wires accessibility onto its single child control; the controls themselves stay native where possible (real keyboard + AT behavior).

Where the order routes

Notes are required

Wraps one control and wires id ↔ label, aria-describedby (hint + error), and aria-invalid.

import { Field, Select } from '@repo/ui/form';
<Field label="Branch" hint="Where the order routes" error={errors.branch}>
<Select value={branch} onChange={e => setBranch(e.target.value)}>
<option value="hq">HQ</option>
</Select>
</Field>
PropTypeNotes
labelstringRendered as the eyebrow-style <label htmlFor>
hintstringHelper text; hidden when error is set
errorstringSets aria-invalid + error styling on the child
requiredbooleanAdds the * marker
idstringOptional; auto-generated via useId otherwise
  • Select β€” styled native <select> with the brand chevron. All <select> props.
  • Textarea β€” styled native <textarea>. All <textarea> props.
  • Checkbox / Radio β€” native inputs tinted via accent-color; optional inline label. Wrap radios in RadioGroup (role=radiogroup, orientation).
  • Switch β€” role=switch toggle. Controlled: checked + onCheckedChange. Keyboard via the underlying button (space/enter).
import { Checkbox, RadioGroup, Radio, Switch } from '@repo/ui/form';
<Checkbox label="Mark as test order" checked={isTest} onChange={…} />
<RadioGroup label="Tier" orientation="horizontal">
<Radio name="tier" value="1" label="Tier 1" />
</RadioGroup>
<Switch checked={on} onCheckedChange={setOn} label="Email notifications" />
  • Do wrap every control in Field so the label and error are wired for screen readers.
  • Do pass an accessible name to Switch (label) β€” it has no text of its own.
  • Don’t use a Switch for choosing between options β€” that’s RadioGroup. A switch is an immediate on/off.
  • Don’t hand-style raw <input>s; reuse these so focus rings and error states stay consistent.
  • Native controls keep native keyboard + AT semantics; Switch uses a real button with role=switch + aria-checked. Focus-visible ring uses --om-orange-press (β‰₯3:1 on light).
  • Verified with jest-axe (zero violations).