Constructing forms in React with React Hook Form
and Zod
.
Enter your credentials to access your dashboard.
1'use client'
2
3import React from 'react'
4import { useForm } from 'react-hook-form'
5import { toast } from 'sonner'
6import { z } from 'zod'
7
8import { Button } from '@/components/ui/button'
9import {
10 Form,
11 FormControl,
12 FormDescription,
13 FormField,
14 FormItem,
15 FormLabel,
16 FormMessage,
17} from '@/components/ui/form'
18import { Input } from '@/components/ui/input'
19import { zodResolver } from '@hookform/resolvers/zod'
20
21const LoginSchema = z.object({
22 email: z.string().email({ message: 'Please Enter a Valid Email' }),
23 password: z
24 .string()
25 .min(6, { message: 'Password must be at least 6 characters' }),
26})
27
28export function FormDemo() {
29 const form = useForm<z.infer<typeof LoginSchema>>({
30 resolver: zodResolver(LoginSchema),
31 defaultValues: {
32 email: 'rafael.costa@example.com',
33 password: 'rafaelCost@123',
34 },
35 })
36
37 function onSubmit(data: z.infer<typeof LoginSchema>) {
38 toast('You have logged in with below email', {
39 description: (
40 <pre className="mt-2 w-[320px] rounded-xl bg-gray-400 p-4">
41 <code className="text-white">
42 {JSON.stringify(data.email, null, 2)}
43 </code>
44 </pre>
45 ),
46 })
47 }
48 return (
49 <div className="flex w-full max-w-sm flex-col items-center justify-center space-y-6">
50 <div className="space-y-1 text-center">
51 <h2 className="text-primary text-2xl font-semibold tracking-tight">
52 Login to your account
53 </h2>
54 <p className="text-muted-foreground text-sm">
55 Enter your credentials to access your dashboard.
56 </p>
57 </div>
58 <Form {...form}>
59 <form
60 onSubmit={form.handleSubmit(onSubmit)}
61 className="w-full max-w-sm min-w-sm space-y-6"
62 >
63 <FormField
64 name="email"
65 control={form.control}
66 render={({ field }) => (
67 <FormItem>
68 <FormLabel>Email</FormLabel>
69 <FormControl>
70 <Input
71 placeholder="Enter your email"
72 type="email"
73 {...field}
74 />
75 </FormControl>
76 <FormDescription>
77 Enter the email you used to register.
78 </FormDescription>
79 <FormMessage className="text-danger" />
80 </FormItem>
81 )}
82 />
83
84 <FormField
85 name="password"
86 control={form.control}
87 render={({ field }) => (
88 <FormItem>
89 <FormLabel>Password</FormLabel>
90 <FormControl>
91 <Input
92 placeholder="Enter your password"
93 type="password"
94 {...field}
95 />
96 </FormControl>
97 <FormDescription></FormDescription>
98 <FormMessage className="text-danger" />
99 </FormItem>
100 )}
101 />
102
103 <Button type="submit">Submit</Button>
104 </form>
105 </Form>
106 </div>
107 )
108}
109
Forms are an essential part of nearly every web application — yet they can often be the most challenging to get right.
A great HTML form should be:
Semantically structured and easy to understand.
User-friendly, with smooth keyboard navigation.
Accessible, leveraging ARIA attributes and clear labels.
Validated both on the client and server for reliability.
Visually consistent with the rest of your design system.
In this guide, we’ll explore how to build robust, accessible forms using React Hook Form
and Zod
, and how to compose them with a <FormField>
component built on Radix UI primitives.
The <Form />
component acts as a convenient wrapper around react-hook-form, designed to make building forms simpler and more consistent.
Composable building blocks for creating flexible form layouts.
A <FormField />
component for managing controlled inputs effortlessly.
Schema-based validation powered by zod
(or any validation library you prefer).
Accessibility handled out of the box, including labels, ARIA attributes, and error messaging.
Automatic unique ID generation using React.useId().
Correct ARIA attributes applied based on form and field state.
Seamless compatibility with all Radix UI components.
You can easily swap out zod
for any other schema validation library that fits your needs.
Full control over structure and styling — the components don’t enforce layout or design decisions.
Install the following dependencies:
We'd love to hear from you! Fill out the form below.
1'use client'
2
3import * as React from 'react'
4import { useForm } from 'react-hook-form'
5import { toast } from 'sonner'
6import { z } from 'zod'
7
8import { Button } from '@/components/ui/button'
9import {
10 Form,
11 FormControl,
12 FormDescription,
13 FormField,
14 FormItem,
15 FormLabel,
16 FormMessage,
17} from '@/components/ui/form'
18import { Input } from '@/components/ui/input'
19import { Textarea } from '@/components/ui/textarea'
20import { zodResolver } from '@hookform/resolvers/zod'
21
22const ContactSchema = z.object({
23 name: z
24 .string()
25 .min(2, { message: 'Name must be at least 2 characters long.' }),
26 email: z.string().email({ message: 'Please enter a valid email address.' }),
27 message: z
28 .string()
29 .min(10, { message: 'Message should be at least 10 characters long.' }),
30})
31
32export function FormContact() {
33 const form = useForm<z.infer<typeof ContactSchema>>({
34 resolver: zodResolver(ContactSchema),
35 defaultValues: {
36 name: '',
37 email: '',
38 message: '',
39 },
40 })
41
42 function onSubmit(data: z.infer<typeof ContactSchema>) {
43 toast('Message sent successfully!', {
44 description: (
45 <pre className="mt-2 w-[320px] rounded-xl bg-gray-400 p-4">
46 <code className="text-white">
47 {JSON.stringify(data, null, 2)}
48 </code>
49 </pre>
50 ),
51 })
52 }
53
54 return (
55 <div className="flex w-full max-w-md flex-col items-center justify-center space-y-6">
56 <div className="space-y-1 text-center">
57 <h2 className="text-primary text-2xl font-semibold tracking-tight">
58 Contact Us
59 </h2>
60 <p className="text-muted-foreground text-sm">
61 We'd love to hear from you! Fill out the form below.
62 </p>
63 </div>
64
65 <Form {...form}>
66 <form
67 onSubmit={form.handleSubmit(onSubmit)}
68 className="w-full space-y-6"
69 >
70 <FormField
71 control={form.control}
72 name="name"
73 render={({ field }) => (
74 <FormItem>
75 <FormLabel>Name</FormLabel>
76 <FormControl>
77 <Input
78 placeholder="Enter your name"
79 {...field}
80 />
81 </FormControl>
82 <FormDescription>
83 Please enter your full name.
84 </FormDescription>
85 <FormMessage className="text-danger" />
86 </FormItem>
87 )}
88 />
89
90 <FormField
91 control={form.control}
92 name="email"
93 render={({ field }) => (
94 <FormItem>
95 <FormLabel>Email</FormLabel>
96 <FormControl>
97 <Input
98 placeholder="you@example.com"
99 type="email"
100 {...field}
101 />
102 </FormControl>
103 <FormDescription>
104 We'll never share your email with
105 anyone.
106 </FormDescription>
107 <FormMessage className="text-danger" />
108 </FormItem>
109 )}
110 />
111
112 <FormField
113 control={form.control}
114 name="message"
115 render={({ field }) => (
116 <FormItem>
117 <FormLabel>Message</FormLabel>
118 <FormControl>
119 <Textarea
120 placeholder="Write your message here..."
121 rows={4}
122 {...field}
123 />
124 </FormControl>
125 <FormDescription>
126 Tell us a bit about what you need help with.
127 </FormDescription>
128 <FormMessage className="text-danger" />
129 </FormItem>
130 )}
131 />
132
133 <Button type="submit" className="w-full">
134 Send Message
135 </Button>
136 </form>
137 </Form>
138 </div>
139 )
140}
141
You can also create forms that seamlessly integrate components like DropdownMenu, RadioGroup, and Input fields — allowing for flexible and interactive form experiences.
Manage your username, role, and notification preferences.
1'use client'
2
3import * as React from 'react'
4import { ChevronDownIcon } from 'lucide-react'
5import { useForm } from 'react-hook-form'
6import { toast } from 'sonner'
7import { z } from 'zod'
8
9import { Button } from '@/components/ui/button'
10import {
11 DropdownMenu,
12 DropdownMenuContent,
13 DropdownMenuItem,
14 DropdownMenuTrigger,
15} from '@/components/ui/dropdown-menu'
16import {
17 Form,
18 FormControl,
19 FormDescription,
20 FormField,
21 FormItem,
22 FormLabel,
23 FormMessage,
24} from '@/components/ui/form'
25import { Input } from '@/components/ui/input'
26import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
27import { Textarea } from '@/components/ui/textarea'
28import { zodResolver } from '@hookform/resolvers/zod'
29
30const FormSchema = z.object({
31 username: z.string().min(2, 'Username must be at least 2 characters.'),
32 role: z.string(),
33 notification: z.string(),
34 bio: z.string().optional(),
35})
36
37export function AccountSettingsForm() {
38 const form = useForm<z.infer<typeof FormSchema>>({
39 resolver: zodResolver(FormSchema),
40 defaultValues: {
41 username: 'rafael costa',
42 role: 'viewer',
43 notification: 'email',
44 bio: '',
45 },
46 })
47
48 function onSubmit(data: z.infer<typeof FormSchema>) {
49 toast('Settings Saved', {
50 description: (
51 <pre className="mt-2 w-[320px] rounded-xl bg-gray-400 p-4">
52 <code className="text-white">
53 {JSON.stringify(data, null, 2)}
54 </code>
55 </pre>
56 ),
57 })
58 }
59
60 const roles = ['admin', 'editor', 'viewer']
61
62 return (
63 <div className="flex w-full max-w-md flex-col items-center justify-center space-y-6">
64 <div className="space-y-1 text-center">
65 <h2 className="text-primary text-2xl font-semibold tracking-tight">
66 Account Settings
67 </h2>
68 <p className="text-muted-foreground text-sm">
69 Manage your username, role, and notification preferences.
70 </p>
71 </div>
72 <Form {...form}>
73 <form
74 onSubmit={form.handleSubmit(onSubmit)}
75 className="w-full space-y-6"
76 >
77 <FormField
78 control={form.control}
79 name="username"
80 render={({ field }) => (
81 <FormItem>
82 <FormLabel>Username</FormLabel>
83 <FormControl>
84 <Input
85 placeholder="Enter your username"
86 {...field}
87 />
88 </FormControl>
89 <FormDescription>
90 This is your public display name.
91 </FormDescription>
92 <FormMessage />
93 </FormItem>
94 )}
95 />
96
97 <FormField
98 control={form.control}
99 name="role"
100 render={({ field }) => (
101 <FormItem>
102 <FormLabel>Role</FormLabel>
103 <FormControl>
104 <DropdownMenu>
105 <DropdownMenuTrigger asChild>
106 <Button
107 variant="outline"
108 className="w-full justify-between"
109 >
110 {field.value || 'Select role'}
111 <ChevronDownIcon className="h-4 w-4 opacity-50" />
112 </Button>
113 </DropdownMenuTrigger>
114 <DropdownMenuContent align="start">
115 {roles.map((r) => (
116 <DropdownMenuItem
117 key={r}
118 onClick={() =>
119 field.onChange(r)
120 }
121 >
122 {r.charAt(0).toUpperCase() +
123 r.slice(1)}
124 </DropdownMenuItem>
125 ))}
126 </DropdownMenuContent>
127 </DropdownMenu>
128 </FormControl>
129 <FormDescription>
130 Choose your permission level.
131 </FormDescription>
132 <FormMessage />
133 </FormItem>
134 )}
135 />
136
137 <FormField
138 control={form.control}
139 name="notification"
140 render={({ field }) => (
141 <FormItem>
142 <FormLabel>Notification Preference</FormLabel>
143 <FormControl>
144 <RadioGroup
145 onValueChange={field.onChange}
146 value={field.value}
147 className="flex flex-row space-x-4"
148 >
149 <div className="flex items-center space-x-2">
150 <RadioGroupItem
151 value="email"
152 id="email"
153 />
154 <FormLabel
155 htmlFor="email"
156 className="font-normal"
157 >
158 Email
159 </FormLabel>
160 </div>
161 <div className="flex items-center space-x-2">
162 <RadioGroupItem
163 value="sms"
164 id="sms"
165 />
166 <FormLabel
167 htmlFor="sms"
168 className="font-normal"
169 >
170 SMS
171 </FormLabel>
172 </div>
173 <div className="flex items-center space-x-2">
174 <RadioGroupItem
175 value="push"
176 id="push"
177 />
178 <FormLabel
179 htmlFor="push"
180 className="font-normal"
181 >
182 Push Notification
183 </FormLabel>
184 </div>
185 </RadioGroup>
186 </FormControl>
187 <FormDescription>
188 How would you like to be notified?
189 </FormDescription>
190 <FormMessage />
191 </FormItem>
192 )}
193 />
194
195 <FormField
196 control={form.control}
197 name="bio"
198 render={({ field }) => (
199 <FormItem>
200 <FormLabel>Bio</FormLabel>
201 <FormControl>
202 <Textarea
203 placeholder="Tell us a little about yourself..."
204 className="w-full rounded-md border bg-transparent p-2 text-sm"
205 rows={4}
206 {...field}
207 />
208 </FormControl>
209 <FormDescription>
210 A short description about you or your work.
211 </FormDescription>
212 <FormMessage />
213 </FormItem>
214 )}
215 />
216
217 <Button type="submit" className="w-full">
218 Save Settings
219 </Button>
220 </form>
221 </Form>
222 </div>
223 )
224}
225
Prop | Type | Default |
---|---|---|
name | string | - |
control | Control | - |
render | ({ field, fieldState }) => ReactNode | - |