components / form
Form
Pass a Zod schema and validate fields without adding another form library.
553 B gzip client componentbase ui · form
form-zod.tsx
"use client";
import { useState } from "react";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
const schema = z.object({
name: z.string().trim().min(2, "enter at least 2 letters."),
email: z.email("that email doesn't look valid."),
password: z.string().min(8, "the password needs 8 characters or more."),
terms: z.literal(true, "accept the terms to continue."),
});
export default function FormZod() {
const [created, setCreated] = useState<string | null>(null);
return (
<Form
className="w-full max-w-sm"
onSubmit={async (values) => {
// `values` is already validated and typed: { name: string; email: string; password: string; terms: true }
await new Promise((r) => setTimeout(r, 800));
setCreated(values.name);
}}
schema={schema}
>
<Field name="name">
<FieldLabel>name</FieldLabel>
<Input autoComplete="name" />
<FieldError />
</Field>
<Field name="email">
<FieldLabel>email</FieldLabel>
<Input autoComplete="email" inputMode="email" />
<FieldError />
</Field>
<Field name="password">
<FieldLabel>password</FieldLabel>
<Input autoComplete="new-password" type="password" />
<FieldError />
</Field>
<Field name="terms">
<FieldLabel className="flex items-center gap-2 font-normal">
<Checkbox /> I accept the terms of use
</FieldLabel>
<FieldError />
</Field>
<Button type="submit" variant="brand">
create account
</Button>
{created && (
<p aria-live="polite" className="text-sm">
account created, {created}!
</p>
)}
</Form>
);
}Installation
$ npx shadcn@latest add https://cd-ui.vercel.app/r/form.jsonfirst time? set up components.json and the theme first: see installation.
Usage
import { z } from "zod"
import { Form } from "@/components/ui/form"
import { Field, FieldError, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
const schema = z.object({ email: z.email("invalid email") })
<Form schema={schema} onSubmit={(values) => save(values)}>
<Field name="email">
<FieldLabel>email</FieldLabel>
<Input inputMode="email" />
<FieldError />
</Field>
</Form>API
Form
Base UI · Form<form> with schema validation. It uses only Zod's core (zod/v4/core), so it accepts schemas from zod and zod/mini.
| prop | type | default |
|---|---|---|
schemaZod schema. Each | ZodObject | - |
onSubmitOnly runs with valid data, already converted and typed. | (values: z.output<schema>) => void | Promise<void> | - |
validationModeWhen each field validates. | "onSubmit" | "onBlur" | "onChange" | "onBlur" |
errorsErrors coming from outside (e.g. a server response). | Record<string, string | string[]> | - |
classNameExtra classes, merged with | string | - |
Accessibility
- Errors appear in each field's
FieldErrorand are linked to the control througharia-describedby. - On submit with errors, focus moves to the first invalid field (Base UI behavior).
- With a schema, prefer
inputMode="email"overtype="email": the native type makes the browser show its own message (in the system language) instead of the schema's.