Form
Accessible form primitives built on react-hook-form
View MarkdownOverview
Form is a set of composable primitives that connect your fields to react-hook-form while wiring up the accessibility relationships between labels, controls, descriptions, and error messages for you.
Each field is wrapped in a FormField (a typed Controller) and a FormItem, which generates the ids that link a Label, the control, its description, and its validation message via htmlFor and aria-describedby. When a field has an error, the label takes on an error style and the control receives aria-invalid, so validation state is conveyed to assistive technology automatically.
The primitives are unstyled containers around your own inputs — pair them with Input, Textarea, Select, Checkbox, and other controls.
Usage
Import the primitives you need:
import {
Form,
FormField,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
} from "@eqtylab/equality";
Create a form with useForm, spread it into Form, and compose each field:
import { useForm } from "react-hook-form";
const form = useForm({ defaultValues: { username: "" } });
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="username"
rules={{ required: "Username is required" }}
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Enter username" {...field} />
</FormControl>
<FormDescription>Your public display name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>;
FormControl wraps a single form control and forwards the accessibility attributes to it via a Radix Slot, so the child must accept a ref and standard input props. FormMessage renders the field’s validation error automatically — you can also pass custom children, and it renders nothing when there is no message.
Examples
Default Form
Form with Validation Errors
Validation rules are declared on FormField via rules. When validation fails, FormMessage displays the error and the label switches to its error style.
Form with Description
FormDescription renders helper text that is linked to the control through aria-describedby.
Custom Field Parts
The built-in parts cover most needs, but you can build your own with the exported useFormField() hook. Called inside a FormField, it returns the current field’s linking ids and its react-hook-form state (error, isDirty, isTouched, and so on), so a custom part can react to validation without prop-drilling.
For example, a status icon that appears once the field has been edited:
import { useFormField } from "@eqtylab/equality";
import { Check, TriangleAlert } from "lucide-react";
function FieldStatus() {
const { error, isDirty } = useFormField();
if (!isDirty) return null;
return error ? (
<TriangleAlert className="text-text-failure" />
) : (
<Check className="text-text-success" />
);
}
Drop it inside a FormItem, alongside the other parts:
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" {...field} />
</FormControl>
<FieldStatus />
<FormMessage />
</FormItem>
useFormField() only works inside a FormField (it reads the field via context) and throws otherwise.
Slots
Compose a form from the following parts:
| Name | Description |
|---|---|
Form | Provider that shares the useForm instance with the fields. Spread your form object onto it. |
FormField | Connects a named field to react-hook-form. Wraps a Controller; use its render prop for the field. |
FormItem | Groups a single field’s parts and generates the linking ids. |
FormLabel | Label for the control. Automatically targets the control and reflects error state. |
FormControl | Wraps the actual input, forwarding id, aria-describedby, and aria-invalid. |
FormDescription | Helper text linked to the control via aria-describedby. |
FormMessage | Displays the field’s validation error (or custom children); renders nothing when empty. |
Props
Form receives the object returned by useForm. FormField forwards all react-hook-form Controller props; the most commonly used are listed below. The remaining parts accept standard element attributes plus className.
| Name | Applies to | Description | Type | Required |
|---|---|---|---|---|
control | FormField | The control object from useForm. | Control | ✅ |
name | FormField | The field name; must match a key in your form values. | string | ✅ |
render | FormField | Render function receiving field (and fieldState) to render the input. | ({ field, fieldState }) => ReactElement | ✅ |
rules | FormField | Validation rules applied to the field. | RegisterOptions | ❌ |