apps/design-system/content/docs/components/form.mdx
Forms are tricky. They are one of the most common things you'll build in a web application, but also one of the most complex.
Well-designed HTML forms are:
In this guide, we will take a look at building forms with react-hook-form and zod. We're going to use a <FormField> component to compose accessible forms using Radix UI components.
The <Form /> component is a wrapper around the react-hook-form library. It provides a few things:
<FormField /> component for building controlled form fields.zod.React.useId() for generating unique IDs.aria attributes to form fields based on states.zod but you can use anything you want.<Form>
<FormField
control={...}
name="..."
render={() => (
<FormItem>
<FormLabel />
<FormControl>
{ /* Your form field */}
</FormControl>
<FormDescription />
<FormMessage />
</FormItem>
)}
/>
</Form>
const form = useForm()
<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>
)}
/>
npx shadcn-ui@latest add form
<Step>Install the following dependencies:</Step>
npm install @radix-ui/react-label @radix-ui/react-slot react-hook-form @hookform/resolvers zod
<Step>Copy and paste the following code into your project.</Step>
<ComponentSource name="form" /><Step>Update the import paths to match your project setup.</Step>
</Steps> </TabsContent> </Tabs>Define the shape of your form using a Zod schema. You can read more about using Zod in the Zod documentation.
'use client'
import { z } from 'zod'
const formSchema = z.object({
username: z.string().min(2).max(50),
})
Use the useForm hook from react-hook-form to create a form.
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
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 = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: '',
},
})
// 2. Define a submit handler.
function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values.
// ✅ This will be type-safe and validated.
console.log(values)
}
}
Since FormField is using a controlled component, you need to provide a default value for the field. See the React Hook Form docs to learn more about controlled components.
We can now use the <Form /> components to build our form.
'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,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} 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 {...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>
)
}
That's it. You now have a fully accessible form that is type-safe with client-side validation.
<ComponentPreview name="input-form" className="[&_[role=tablist]]:hidden [&>div>div:first-child]:hidden" />
</Steps>See the following links for more examples on how to use the <Form /> component with other components: