Form
The frame around Fields: Base UI's form, which validates every Field on submit and shows errors handed back from the server, with fieldsets under leaning legends.
Install
# once per project: where @zeroui items come frompnpm dlx shadcn@latest registry add @zeroui=https://zeroui.catforest.dev/r/{name}.jsonpnpm dlx shadcn@latest add @zeroui/formExamples
Post a job, with an error from the server
import { useState } from "react";import { Button } from "@/components/zero/button";import { Field, FieldDescription, FieldError, FieldLabel,} from "@/components/zero/field";import { Fieldset, FieldsetLegend, Form, FormActions,} from "@/components/zero/form";import { Input } from "@/components/zero/input";import { Textarea } from "@/components/zero/textarea";export default function FormJob() { const [errors, setErrors] = useState<Record<string, string>>({}); const [posted, setPosted] = useState(""); return ( <Form className="w-full max-w-md" errors={errors} onFormSubmit={(values) => { // Stands in for the server: it rejects a reward under 500. if (Number(values.reward) < 500) { setErrors({ reward: "The dispatcher posts nothing under 500 cr." }); return; } setErrors({}); setPosted(String(values.title)); }} > <Fieldset> <FieldsetLegend>Post a job</FieldsetLegend> <Field name="title"> <FieldLabel>Title</FieldLabel> <Input required placeholder="Sealed crate" /> <FieldError>Give the job a title.</FieldError> </Field> <Field name="reward" validate={(value) => (value ? null : "Name the reward.")} > <FieldLabel>Reward</FieldLabel> <Input type="number" inputMode="numeric" placeholder="3100" /> <FieldDescription>In credits, paid on hand-in.</FieldDescription> {/* its own message, or the server's, from the form's errors */} <FieldError /> </Field> <Field name="notes"> <FieldLabel>Notes</FieldLabel> <Textarea placeholder="Anything the driver should know" /> </Field> </Fieldset> <FormActions> <Button type="submit">Post it</Button> {posted ? ( <span className="font-medium font-zero-text text-zero-sm text-zero-accent"> Posted: {posted} </span> ) : null} </FormActions> </Form> );}With react-hook-form and zod
import { zodResolver } from "@hookform/resolvers/zod";import { Controller, useForm } from "react-hook-form";import { z } from "zod";import { Button } from "@/components/zero/button";import { Field, FieldError, FieldLabel } from "@/components/zero/field";import { Form, FormActions } from "@/components/zero/form";import { Input } from "@/components/zero/input";const schema = z.object({ driver: z.string().min(2, "At least two letters."), code: z .string() .regex(/^[A-Z0-9]{4}$/, "Four letters or digits, in capitals."),});type Values = z.infer<typeof schema>;export default function FormHook() { const form = useForm<Values>({ resolver: zodResolver(schema), defaultValues: { driver: "", code: "" }, }); return ( <Form className="w-full max-w-md" onSubmit={form.handleSubmit(() => form.reset())} > {(["driver", "code"] as const).map((name) => ( <Controller key={name} name={name} control={form.control} render={({ field, fieldState }) => ( <Field name={name} invalid={fieldState.invalid}> <FieldLabel> {name === "driver" ? "Driver" : "Gate code"} </FieldLabel> <Input {...field} /> <FieldError match={fieldState.invalid}> {fieldState.error?.message} </FieldError> </Field> )} /> ))} <FormActions> <Button type="submit">Sign for it</Button> </FormActions> </Form> );}Behaviour
Form collects every Field's value and runs their validation on submit; errors, keyed by each Field's name, puts a server's messages in the Fields' FieldError. Fieldset groups Fields under a legend led by a slanted sliver; disabling it disables everything inside. FormActions is the row of buttons at the end.
With react-hook-form, wire each control through its Controller and give the Field invalid and the FieldError match from the field's state, as in the second example.
Props
Form
| Prop | Type | Default | Description |
|---|---|---|---|
| errors | Record<string, string | string[]> | — | Messages from the server, by field name. |
| onFormSubmit | (values) => void | — | Called with every Field's value once they are all valid. |
Also takes every prop of Base UI's Form.
Fieldset, FieldsetLegend
| Prop | Type | Default | Description |
|---|---|---|---|
| disabled | boolean | false | Disables every control inside. |
Also takes every prop of Base UI's Fieldset.Root.