Form
React Hook Form scaffolding that wires labels, controls, descriptions, and validation messages.
@garn/ui/formOn this page
Default
A single RHF-bound, labeled field with description + submit.
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add form- Registry
form- Deps
react-hook-form@radix-ui/react-slot- Registry deps
utilslabel
Import
import { Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from "@garn/ui/form";Anatomy
The parts this component is built from — compose them to assemble it.
FormRequired= RHF FormProvider. Spread your useForm() return into it.
FormFieldRequiredBinds a field by name to a control via RHF's Controller (render-prop).
FormItemRequired[data-slot="form-item"]One field's row; provides the id that ties label/control/message together.
FormLabel[data-slot="form-label"]Label wired to the control (htmlFor), turns destructive on error.
FormControlRequired[data-slot="form-control"]Slot that injects id + aria-describedby + aria-invalid onto your control.
FormDescription[data-slot="form-description"]Helper text, linked via aria-describedby.
FormMessage[data-slot="form-message"]Validation error, linked via aria-describedby; renders nothing when valid.
Props
The component's public props and their types.
No component-specific props — this is a thin wrapper over its native element.
Styling
Target these data-slots and states, and remap these tokens, to restyle without forking the component.
[data-slot="form-item"]States
| Selector | State | Description |
|---|---|---|
[aria-invalid=true] | invalid | FormControl sets aria-invalid; FormLabel + FormMessage turn destructive. |
| Property | Token | Tier |
|---|---|---|
fieldGap | --garn-gap-field | semantic · air |
When to use
Build accessible forms with React Hook Form — consistent label / control / description / error wiring out of the box.
Reach for something else when
- A single, simple field with no validation (just use the control + a Label)
- a form not using react-hook-form.
Overview
Form is the accessible wiring layer over React Hook Form: it ties each field's label, control, description, and error message together so the ARIA plumbing is correct by default. Form is the RHF provider, FormField binds a name via Controller, and within a FormItem the FormLabel / FormControl / FormDescription / FormMessage parts share one id set — FormControl injects id + aria-describedby + aria-invalid onto your control. Use it for any real form; a single uncontrolled field doesn't need the machinery.
Guidelines
Wrap each field in FormField + FormItem. That's what generates the shared id and links label↔control↔description↔error; reaching past it to hand-wire htmlFor/aria-* reintroduces the bugs Form exists to prevent. Put your control inside FormControl so the a11y attributes land on it.
Validate with a schema and surface errors inline. Drive RHF with a resolver (Zod/Yup) so FormMessage renders the real validation message beside the field and aria-invalid styles it; show errors on submit/blur, not on every keystroke. Keep one clear message per field.
Write helpful labels and descriptions. Use FormDescription for format hints and requirements rather than cramming them into the label or a placeholder, mark optional fields explicitly, and keep the submit action's state honest (disable/spinner while pending).
Best practices
- Wrap fields in FormField (control + name) and render the control inside FormControl.
- Put one FormItem per field so the label/control/message ids stay linked.
- Let FormMessage render the error — it pulls from RHF field state automatically.
- Don't hand-wire aria-describedby/aria-invalid — FormControl does it.
- Don't call useFormField outside a FormField — it throws.
Content guidelines
- Write field labels as short noun phrases; reserve helper text for one line of guidance.
- Make error messages specific and fixable (“Enter a valid email”), tied to the field.
Troubleshooting
useFormField should be used within <FormField> thrown at runtime.
Cause. A Form* part rendered outside a FormField/FormItem.
Fix. Wrap each field's parts in FormField → FormItem.
The field doesn't track input or validate.
Cause. The control isn't wired to RHF.
Fix. Render the control inside FormControl and spread the Controller field onto it.
Accessibility
- Role
form
- FormControl auto-wires id + aria-describedby (description + message) + aria-invalid onto the control.
- FormLabel's htmlFor targets the control; FormMessage announces the validation error.