How to compose, validate, and submit forms with OUI — including React Hook Form + Zod.
This guide covers the cross-cutting patterns for building forms with OUI: composition with React Aria's Form, validation strategies, controlled vs uncontrolled state, React Hook Form + Zod integration, server-side errors, when to drop down to Field primitives, and layout conventions.
For per-component details, see the individual component docs (e.g., TextField, Select).
| You need… | Use |
|---|---|
| Single-line text | TextField |
| Multi-line text | TextAreaField |
| Numbers with steppers | NumberField |
| Phone numbers | PhoneNumberField |
| Search (with a clear button) | SearchField |
| Dates (keyboard entry) | DateField |
| Dates with a calendar popover | DatePicker |
| Date ranges | DateRangePicker |
| Times | TimeField |
| Single choice from a short list (dropdown) | Select |
| Single choice with type-ahead filtering | ComboBox |
| Multiple choices with filtering | TagField |
| Single choice from a small set (visible) | RadioGroup |
| Multiple toggles (visible) | CheckboxGroup / Checkbox |
| On/off | Toggle |
| File uploads | FileDropzone |
OUI fields are designed to be wrapped by React Aria's <Form> element. Each field's name prop becomes a key in the submitted FormData. The onSubmit handler receives a standard form-submit event.
Key points:
name to be included in submission.<Form> triggers native HTML validation by default — invalid fields block submission and surface their constraint violations.flex flex-col gap-6 for vertical stacking with consistent spacing.OUI inherits React Aria's validation model. There are three layers:
Set isRequired, minLength, maxLength, pattern, or type on the field. Errors appear after the user commits a value (on blur) or submits the form.
The validate prop accepts a function that returns an error message string when invalid, or null / true when valid.
For immediate feedback as the user types (e.g., password strength), use isInvalid + errorMessage with controlled state. This bypasses the commit-then-validate flow.
validationBehaviorThe validationBehavior prop on <Form> (or on individual fields) controls enforcement:
"native" (default) — Uses native HTML form validation. Invalid fields prevent submission and show browser-default error styling unless FieldError is rendered."aria" — Marks invalid fields via ARIA attributes only. Submission is allowed regardless of validity.Use "aria" when you're integrating with React Hook Form, Zod, or any external validation library that needs to control the submission flow itself. Use "native" when OUI's built-in constraints are sufficient.
For uncontrolled forms, onSubmit receives the raw event; pull FormData from e.currentTarget:
<Form
onSubmit={(e) => {
e.preventDefault()
const data = Object.fromEntries(new FormData(e.currentTarget))
submit(data)
}}
>
...
</Form>For async submissions, manage a submitting state and swap the submit button's children:
For complex forms, most production OUI applications use React Hook Form with Zod for schema-driven validation. Set validationBehavior="aria" on <Form> so native validation doesn't interfere with RHF's submission flow.
Wrap each OUI field in RHF's Controller, since OUI fields are controlled-friendly but their value shapes vary (string for TextField, key for Select, set of keys for TagField).
Notes:
errorMessage accepts a string OR a render prop. Here we pass errors.<field>?.message from RHF.value (a Key | null) and onChange. Coerce in both directions.string[]; OUI's selectedKeys is Set<Key>. Convert at the boundary.After submit, set isInvalid and errorMessage on the affected field with the server's response. Render-side error messages compose seamlessly with client-side validation.
For form-level errors (e.g., "Your session expired"), render an Infobox above the form. Screen readers announce it when it appears if the container has role="alert" or aria-live="polite".
Use the higher-level wrapper components (TextField, Select, etc.) whenever possible — they bundle label, description, error, and accessibility wiring. Drop down to Field primitives when you need:
The Field docs cover the full set of primitives — Label, Description, FieldError, FieldErrorIcon, FieldGroup.
OUI fields stack their internal parts (label, input, description, error) with built-in spacing. You're responsible for the spacing between fields.
The convention used in OUI's own examples:
<Form className="flex flex-col gap-6">...</Form>gap-6 (1.5rem) gives consistent vertical rhythm between fields without making the form feel sparse. Adjust for dense forms (try gap-4) or spacious ones (gap-8).
For the submit button, place it at the bottom of the form, full-width on mobile, fixed-width on desktop:
<Button type="submit" className="self-end sm:w-auto">
Submit
</Button>