Form
<Form> is a thin layer on top of react-hook-form that ties <FormLabel>, <FormControl>, <FormDescription>, and <FormMessage> together with the right ids and aria-describedby references. Use it for every multi-field form.
When to use
- Any form with two or more fields.
- Any field that needs validation, an error message, or a description.
Don’t use Form for: a single-field “search” input (just use Input), inline-edit per-property fields (InlineEditField), or wizards with branching logic (use Stepper — planned).
Basic
import { useForm } from 'react-hook-form';import { Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage,} from '@na/ui/components/form';
const form = useForm<AgentValues>({ defaultValues: { name: '', description: '' } });
<Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <FormField control={form.control} name="name" render={({ field }) => ( <FormItem> <FormLabel>Name</FormLabel> <FormControl> <Input placeholder="Acme Corp" {...field} /> </FormControl> <FormDescription>The display name shown to your team.</FormDescription> <FormMessage /> </FormItem> )} /> {/* …more FormFields… */} <Button type="submit">Save</Button> </form></Form>How the parts compose
Each <FormField> provides a context. Inside, the slot wrappers wire:
<FormLabel>→ renders a<Label>withhtmlForset to the input’sid.<FormControl>→ forwards the rightid,aria-describedby(linking the description and message), andaria-invalid.<FormDescription>→ renders a muted helper paragraph.<FormMessage>→ renders the field’s validation error (from react-hook-form), or the children when there’s no error.
You write the React markup; the wiring is handled.
API
Form
Re-exported FormProvider from react-hook-form. Spread the useForm() return value onto it.
FormField
| Prop | Type | Default | Description |
|---|---|---|---|
control * | Control | — | Pass form.control. |
name * | string | — | Field name within the form values type. |
render * | (props: { field, fieldState, formState }) => ReactNode | — | Render the input. Spread `field` onto your input component. |
rules | object | — | react-hook-form validation rules. |
defaultValue | unknown | — | Field-level default (overrides form-level). |
FormItem
A <div> wrapper that owns the per-field grid + spacing. Wrap each FormField’s render output in <FormItem>.
FormLabel
Composes Label — applies htmlFor and an error-state color when validation fails.
FormControl
Composes the input via Radix Slot. Forwards the right id, aria-describedby, and aria-invalid.
FormDescription
A muted <p> rendered above the message slot. Tied to the input via aria-describedby.
FormMessage
A destructive <p> rendered below. Renders the validation error message when present, otherwise its children, otherwise nothing.
useFormField
Hook returning { id, name, formItemId, formDescriptionId, formMessageId, error, … } for advanced custom layouts.
Design guidelines
✓ Do
- Use Form for multi-field forms. The id wiring + aria are not optional.
- Validate with react-hook-form rules or a resolver (zod, yup). FormMessage picks up errors automatically.
- Pair every field with a FormDescription when the label alone leaves room for ambiguity.
✗ Don't
- Forget to render <FormMessage />. Without it the user sees errors only via the destructive ring.
- Spread {...field} onto a wrapper instead of the input. The field props belong on the input itself.
- Use Form for inline-edit fields. That's a different primitive — InlineEditField.
Accessibility
- Every form field is fully wired:
<input>getsid,<label>getshtmlFor, error and description are linked viaaria-describedby, invalid state setsaria-invalid. - Use a single
<form onSubmit>per<Form>. Submit on Enter inside any input is automatic. - Disable Submit while async validation is pending — keep the user from re-submitting.
Related
Input,Textarea,Select,Checkbox,Switch,RadioGroup,Slider— Spreadfieldonto any of these.FormError— Top-of-form root error (server-returned, network, etc.).FormSection— Group of related fields with a title.ActionButton— Submit button with mutation state.