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 createFormHook 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 { useAppForm } = createFormHook()
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 createFormHook function to create a form hook, then use the useAppForm hook to define your form with validation and submit handling.

components/example-form.tsx
"use client"

import { z } from "zod"

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

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

// 1. Create a form hook
const { useAppForm } = createFormHook()

export function ProfileForm() {
  // 2. Define your form.
  const form = useAppForm({
    defaultValues: { username: "" } as z.infer<typeof formSchema>,
    validators: { onChange: formSchema },
    // 3. 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 { createFormHook } 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.",
  })
})

const { useAppForm } = createFormHook()

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>
  );
}

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

Create a form hook using the createFormHook function imported from @/components/ui/form. Ensure this is defined outside of your component.

const { useAppForm } = createFormHook()

export function ProfileForm() {
  // ...
}

Use the useAppForm hook 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>
  );
}