Installation
npx logic2b@next add formAnatomy
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
const formSchema = z.object({
username: z.string().min(2),
})
function ProfileForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { username: "" },
})
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(console.log)}>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<input {...field} />
</FormControl>
<FormDescription>Your public display name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
)
}
FormField wraps react-hook-form’s Controller and provides field context to
FormLabel, FormControl, FormDescription and FormMessage — they read
validation state (error, id) from that context, so labels and messages stay
wired to the right input automatically.
Usage notes
Form is an alias for react-hook-form’s FormProvider, so spread your useForm
return value onto it. The pieces below consume field context.
Examples
Sign In
Validate an email and password with zod.
Textarea
Accessibility
FormControlsetsid,aria-describedbyandaria-invalidfrom field context, so the label, description and error message are all wired to the input without manualhtmlFor/idbookkeeping.- On validation failure
FormMessagerenders the error text with the id referenced byaria-describedby, andFormLabelturns destructive — the invalid state is conveyed both visually and to screen readers. - Because each control gets a unique generated id, labels always point at the right input even when the field is reused in a list.
Live playground
Edit the component props and inspect the exact JSX before copying it. The preview loads only when this section enters the viewport.
const form = useForm({
defaultValues: { username: "" },
})
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(console.log)}>
<FormField
control={form.control}
name="username"
rules={{ required: "Username is required." }}
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input
{...field}
placeholder={"logic2b"}
/>
</FormControl>
<FormDescription>{"This is your public display name."}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Save profile</Button>
</form>
</Form>
)API reference
Generated from the public TypeScript exports in src/ui/form.tsx.
useFormField
Hook(): { invalid: boolean; isDirty: boolean; isTouched: boolean; isValidating: boolean; error?: FieldError; id: string; name: string; formItemId: string; formDescriptionId: string; formMessageId: string; }Form
ComponentDirect alias of FormProvider.
FormItem
ComponentProps: React.ComponentProps<"div">FormLabel
ComponentProps: React.ComponentProps<typeof Label>FormControl
ComponentProps: React.ComponentProps<typeof Slot.Root>FormDescription
ComponentProps: React.ComponentProps<"p">FormMessage
ComponentProps: React.ComponentProps<"p">FormField
ComponentProps: ControllerProps<TFieldValues, TName>Accessibility contract
Machine-readable behavior and consumer responsibilities for form.
- Support
- Authored behavior
- Pattern
- form validation
- React Hook Form + Radix Label
Built-in semantics
- FormControl derives id, aria-invalid and aria-describedby from field state.
- Descriptions and error messages receive stable ids.
Consumer responsibilities
- Render FormLabel and useful error text; move focus or provide a summary after failed submission.