Important: We now recommend using the official shadcn/ui + Base UI setup for new projects.Learn more
basecn
Components

Form (TanStack Form)

Building forms with TanStack Form and Zod.

This is your public display name.


This guide demonstrates how to build forms using @tanstack/react-form and zod. We'll use the useAppForm hook to compose accessible forms.

Features

  • Composable components for building forms.
  • Built-in field components for building controlled form fields.
  • Form validation using zod or any other validation library.
  • Handles accessibility and error messages.
  • Uses React.useId() for generating unique IDs.
  • Applies the correct aria attributes to form fields based on states.
  • Bring your own schema library. We use zod but you can use anything you want.
  • You have full control over the markup and styling.

Anatomy

<form.AppForm>
  <form.AppField name="...">
    {(field) => (
      <form.Item>
        <field.Label />
        <field.Control>
          { /* Your form field */}
        </field.Control>
        <field.Description />
        <field.Message />
      </form.Item>
    )}
  </form.AppField>
</form.AppForm>

Example

const form = useAppForm({...})

<form.AppForm>
  <form.AppField name="username">
    {(field) => (
      <form.Item>
        <field.Label>Username</field.Label>
        <field.Control>
          <Input
            placeholder="shadcn"
            name={field.name}
            value={field.state.value}
            onBlur={field.handleBlur}
            onChange={(e) => field.handleChange(e.target.value)}
          />
        </field.Control>
        <field.Description>This is your public display name.</field.Description>
        <field.Message />
      </form.Item>
    )}
  </form.AppField>
</form.AppForm>

Installation

pnpm dlx shadcn@latest add https://basecn.dev/r/form-tanstack.json
or

If you are using a namespaced registry, you can use the following command:

pnpm dlx shadcn@latest add @basecn/form-tanstack

Usage

Create a form schema

Define the shape of your form using a Zod schema. You can read more about using Zod in the Zod documentation.

components/example-form.tsx
"use client"

import { z } from "zod"

const formSchema = z.object({
  username: z.string().min(2).max(50),
})

Define a form

Use the useAppForm hook to define your form with validation and submit handling.

components/example-form.tsx
"use client"

import { z } from "zod"

import { useAppForm } from "@/components/ui/form";

const formSchema = z.object({
  username: z.string().min(2, {
    message: "Username must be at least 2 characters.",
  }),
})

export function ProfileForm() {
  // 1. Define your form.
  const form = useAppForm({
    defaultValues: { username: "" } as z.infer<typeof formSchema>,
    validators: { onChange: formSchema },
    // 2. Define a submit handler.
    onSubmit({ value }) {
      // Do something with the form values.
      // ✅ This will be type-safe and validated.
      console.log(value);
    },
  })
}

Build your form

Use the form.AppForm component to wrap your form.

components/example-form.tsx
"use client"

import { z } from "zod"

import { Button } from "@/components/ui/button"
import { useAppForm } from "@/components/ui/form"
import { Input } from "@/components/ui/input"

const formSchema = z.object({
  username: z.string().min(2, {
    message: "Username must be at least 2 characters.",
  })
})

export function ProfileForm() {
  // ...

  return (
    <form.AppForm>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          form.handleSubmit();
        }}
        className="space-y-8"
      >
        <form.AppField name="username">
          {(field) => (
            <form.Item>
              <field.Label>Username</field.Label>
              <field.Control>
                <Input
                  placeholder="shadcn"
                  name={field.name}
                  value={field.state.value}
                  onBlur={field.handleBlur}
                  onChange={(e) => field.handleChange(e.target.value)}
                />
              </field.Control>
              <field.Description>
                This is your public display name.
              </field.Description>
              <field.Message />
            </form.Item>
          )}
        </form.AppField>

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

Add pre-bound components

Since you own the code, you can add custom field and form components to your form hook. You can read more about using pre-bound components in the TanStack Form documentation.

components/ui/form.tsx
const { useAppForm } = createFormHook({
  fieldComponents: {
    Label: FieldLabel,
    Control: FieldControl,
    Description: FieldDescription,
    Message: FieldMessage,
    Select: FieldSelect,
    // ...
  },
  formComponents: {
    Item: FormItem,
    Submit: SubmitButton,
    // ...
  },
  fieldContext,
  formContext,
});

Examples

Checkbox Form

Select the items you want to display in the sidebar.

Radio Form

Select Form

You can manage email addresses in your email settings.

Switch Form

Email Notifications

Receive emails about new products, features, and more.

Receive emails about your account security.

Textarea Form

You can @mention other users and organizations.

Combobox Form

This is the language that will be used in the dashboard.

Date Picker Form

Your date of birth is used to calculate your age.


Migrating from React Hook Form

Define a form hook

Use the useAppForm hook imported from @/components/ui/form, instead of useForm to define your form with validation and submit handling.

export function ProfileForm() {
  const form = useAppForm({
    defaultValues: { username: "" } as z.infer<typeof formSchema>,
    validators: { onChange: formSchema },
    onSubmit({ value }) {
      // Do something with the form values.
      // ✅ This will be type-safe and validated.
      console.log(value);
    },
  })
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
    },
  })

  function onSubmit(values: z.infer<typeof formSchema>) {
    // Do something with the form values.
    // ✅ This will be type-safe and validated.
    console.log(values)
  }
}

Define a form

Define a form using the <form.AppForm> component.

export function ProfileForm() {
  // ...

  return (
    <form.AppForm>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          form.handleSubmit();
        }}
        className="space-y-8"
      >
        <form.AppField name="username">
          {(field) => (
            <form.Item>
              <field.Label>Username</field.Label>
              <field.Control>
                <Input
                  placeholder="shadcn"
                  name={field.name}
                  value={field.state.value}
                  onBlur={field.handleBlur}
                  onChange={(e) => field.handleChange(e.target.value)}
                />
              </field.Control>
              <field.Description>
                This is your public display name.
              </field.Description>
              <field.Message />
            </form.Item>
          )}
        </form.AppField>

        <Button type="submit">Submit</Button>
      </form>
    </form.AppForm>
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Username</FormLabel>
              <FormControl>
                <Input placeholder="shadcn" {...field} />
              </FormControl>
              <FormDescription>
                This is your public display name.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  );
}

On this page