use UI

Form Builder

Build a form by adding and ordering fields, then copy a component that compiles as it stands: zod schema, typed values, react-hook-form wiring and shadcn/ui controls, with only the imports the form actually needs.

  • Email
  • Password

Add a field

Preview

Email *
Password *
Submit

SignInForm.tsx

"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";

const formSchema = z.object({
  email: z.string().email("Email must be a valid email address.").min(1, "Email is required."),
  password: z.string().min(8, "Password must be at least 8 characters."),
});

type FormValues = z.infer<typeof formSchema>;

export function SignInForm() {
  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      email: "",
      password: "",
    },
  });

  function onSubmit(values: FormValues) {
    console.log(values);
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input type="email" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="password"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Password</FormLabel>
              <FormControl>
                <Input type="password" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <Button type="submit">Submit</Button>
      </form>
    </Form>
  );
}

Frequently asked

Does the generated code actually run?

Yes — that is the point. It includes the imports, the zod schema, the inferred type, defaultValues for every field, and a FormField block per control. Paste it into a project that has shadcn/ui's form components installed and it compiles. Only the imports the form needs are emitted, so a form with no select does not import Select.

Why zod rather than validating by hand?

Because the schema gives you the TypeScript type for free through z.infer, so the form values and your submit handler cannot drift apart. It also puts the message next to the rule, which is where it stays correct — validation split across a schema and a component is validation that eventually disagrees with itself.

Why is a number field coerced?

Because an input always hands back a string, even with type=number. Without z.coerce.number() the schema receives "42" and rejects it as the wrong type, which looks like a bug in your form rather than a type mismatch.

What does required actually change?

It changes the schema, not the markup. A required text field gets .min(1) with a message; an optional one becomes .optional().or(z.literal("")) so an untouched empty input passes. A required checkbox gets a refine that insists on true — which is how you model a terms-and-conditions box.

Is there drag and drop?

No, deliberately. Ordering uses up and down buttons because they work with a keyboard, a screen reader and a touch screen without any extra code. Drag and drop looks better in a screenshot and is worse for everyone who is not using a mouse.

Related tools