Form
A wrapper for Base UI Form with server-side error propagation, JS object submission via onFormSubmit, global validationMode, and imperative validate() via actionsRef.
Installation
npx shadcn@latest add @glrk-ui/formIf you haven't set up the prerequisites yet, check out Prerequest section.
Copy and paste the following code into your project.
'use client'
import { Form as FormPrimitive } from '@base-ui/react/form'
import { cn } from '@/lib/utils'
function Form({ className, ...props }: React.ComponentProps<typeof FormPrimitive>) {
return (
<FormPrimitive
data-slot="form"
className={cn('flex flex-col gap-4', className)}
{...props}
/>
)
}
export { Form }
Usage
Basic
Form renders a native <form>. Use it with Field and FieldNativeError for native validation, or with field-wrapper components for controlled validation.
import { Form } from "@/components/ui/form"
import { InputWrapper } from "@/components/ui/field-wrapper"
<Form onSubmit={handleSubmit} className="flex flex-col gap-4">
<InputWrapper name="email" label="Email" type="email" />
<button type="submit">Submit</button>
</Form>Server-side errors
Pass a Record<fieldName, message> to errors. Errors propagate via context to Field.Root components with matching name and are displayed by FieldNativeError:
import { Form } from "@/components/ui/form"
import { Field, FieldLabel, FieldNativeError } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
const [errors, setErrors] = useState<Record<string, string>>({})
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
const res = await submitForm()
if (res.errors) setErrors(res.errors)
}
<Form errors={errors} onSubmit={handleSubmit}>
<Field name="email">
<FieldLabel>Email</FieldLabel>
<Input name="email" type="email" onChange={() => setErrors({})} />
<FieldNativeError /> {/* shows errors.email from Form context */}
</Field>
</Form>JS object submission (onFormSubmit)
onFormSubmit receives parsed form values as a plain object. preventDefault is called automatically:
<Form
onFormSubmit={(values) => {
console.log(values) // { email: "...", name: "..." }
}}
>
<Field name="email">
<FieldLabel>Email</FieldLabel>
<Input name="email" required />
<FieldNativeError match="valueMissing">Required</FieldNativeError>
</Field>
</Form>Native validation
Use Field validate prop with FieldNativeError for custom validation. Set validationMode globally on Form:
<Form validationMode="onChange" onFormSubmit={handleSubmit}>
<Field
name="username"
validate={(val) => {
if (!val) return "Required"
if ((val as string).length < 3) return "Min 3 characters"
return null
}}
>
<FieldLabel>Username</FieldLabel>
<Input name="username" />
<FieldNativeError />
</Field>
</Form>HTML constraint validation
<Form onFormSubmit={handleSubmit}>
<Field name="email">
<FieldLabel>Email</FieldLabel>
<Input name="email" type="email" required />
<FieldNativeError match="valueMissing">Email is required</FieldNativeError>
<FieldNativeError match="typeMismatch">Enter a valid email</FieldNativeError>
</Field>
<Field name="password">
<FieldLabel>Password</FieldLabel>
<Input name="password" type="password" required minLength={8} />
<FieldNativeError match="valueMissing">Required</FieldNativeError>
<FieldNativeError match="tooShort">At least 8 characters</FieldNativeError>
</Field>
</Form>Imperative validation
Trigger validate() programmatically without submitting:
const actionsRef = useRef<{ validate: () => void } | null>(null)
<Form actionsRef={actionsRef} onFormSubmit={handleSubmit}>
{/* fields */}
<button type="button" onClick={() => actionsRef.current?.validate()}>
Check
</button>
<button type="submit">Submit</button>
</Form>Reference
Form
Prop
Type