Form
A thin bridge between React Hook Form and Field. Field owns the accessibility wiring and knows nothing about any form library; this adds only state and validation.
src/components/ui/form.tsxAbout
From the component's own source, where it was written.
A thin bridge between React Hook Form and this kit's Field.
The split is deliberate: Field owns the accessibility wiring — id, aria-describedby, aria-invalid — and knows nothing about any form library, so it keeps working with useState, a server action, or nothing at all. This file adds only state and validation on top.
React Hook Form was chosen on the numbers: 241M downloads a month against 10.6M for the nearest actively maintained alternative, zero runtime dependencies, and 26 releases in 2026. Formik has more downloads than TanStack Form but shipped nothing at all in 2026.
Install
npx astralyx-ui add form
Usage
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Form, FormField } from '@/components/ui/form'
const schema = z.object({ email: z.email('Enter a valid email.') })
const form = useForm({ resolver: zodResolver(schema), mode: 'onTouched' })
<Form form={form} onSubmit={save}>
<FormField name="email" label="Email" required>
{(field) => <Input {...field} type="email" />}
</FormField>
<Button type="submit">Save</Button>
</Form>Composer
Change the props and the source below follows.
<Form form={form} onSubmit={save}>
<FormField name="email" label="Email" required>
{(field) => <Input {...field} type="email" />}
</FormField>
</Form>Examples
Validated signup
<Form form={form} onSubmit={save}>…</Form>API
Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.
Form<TFieldValues extends FieldValues, TContext = unknown, TTransformed extends FieldValues = TFieldValues>
A thin bridge between React Hook Form and this kit's Field.
Extends Omit<ComponentProps<'form'>, 'onSubmit'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| form | UseFormReturn<TFieldValues, TContext, TTransformed> | required | The instance from useForm(). Provided to descendants, so nested fields need no prop drilling. |
| onSubmit | (values: TTransformed) => void | Promise<void> | required | Runs only after validation passes. The form is noValidate, so the resolver decides — browser bubbles cannot be styled. |
FormField<T extends FieldValues, N extends FieldPath<T> = FieldPath<T>>
One controlled field, wired to both the form and the accessibility plumbing.
| Prop | Type | Default | Description |
|---|---|---|---|
| name | N | required | Typed against the schema, so a rename is a compile error rather than a silent no-op. |
| label | ReactNode | — | Passed through to Field and FieldLabel. |
| description | ReactNode | — | Passed through to Field and FieldLabel. |
| required | boolean | — | Passed through to Field and FieldLabel. |
| className | string | — | — |
| children | ( field: ControllerRenderProps<T, N> & ReturnType<typeof useFieldControl>, ) => ReactNode | required | Render prop. Half the controls take value/onValueChange and half take checked/onChange, so mapping is the caller's job. |
Behaviour
| Topic | Detail | Description |
|---|---|---|
| errors | automatic | The message from the resolver is handed to Field, which is what flips aria-invalid and shows it. There is no second flag. |