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 { z } from 'zod'
6
7import { toast } from '@/components/ui/use-toast'
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({
39 title: 'You have logged in with below email',
40 description: (
41 <pre className="mt-2 w-[320px] rounded-xl bg-gray-400 p-4">
42 <code className="text-white">
43 {JSON.stringify(data.email, null, 2)}
44 </code>
45 </pre>
46 ),
47 })
48 }
49 return (
50 <div className="flex w-full max-w-sm flex-col items-center justify-center space-y-6">
51 <div className="space-y-1 text-center">
52 <h2 className="text-primary text-2xl font-semibold tracking-tight">
53 Login to your account
54 </h2>
55 <p className="text-muted-foreground text-sm">
56 Enter your credentials to access your dashboard.
57 </p>
58 </div>
59 <Form {...form}>
60 <form
61 onSubmit={form.handleSubmit(onSubmit)}
62 className="w-full max-w-sm min-w-sm space-y-6"
63 >
64 <FormField
65 name="email"
66 control={form.control}
67 render={({ field }) => (
68 <FormItem>
69 <FormLabel>Email</FormLabel>
70 <FormControl>
71 <Input
72 placeholder="Enter your email"
73 type="email"
74 {...field}
75 />
76 </FormControl>
77 <FormDescription>
78 Enter the email you used to register.
79 </FormDescription>
80 <FormMessage className="text-danger" />
81 </FormItem>
82 )}
83 />
84
85 <FormField
86 name="password"
87 control={form.control}
88 render={({ field }) => (
89 <FormItem>
90 <FormLabel>Password</FormLabel>
91 <FormControl>
92 <Input
93 placeholder="Enter your password"
94 type="password"
95 {...field}
96 />
97 </FormControl>
98 <FormDescription></FormDescription>
99 <FormMessage className="text-danger" />
100 </FormItem>
101 )}
102 />
103
104 <Button type="submit">Submit</Button>
105 </form>
106 </Form>
107 </div>
108 )
109}
110
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 { z } from 'zod'
6
7import { toast } from '@/components/ui/use-toast'
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({
44 title: 'Message sent successfully!',
45 description: (
46 <pre className="mt-2 w-[320px] rounded-xl bg-gray-400 p-4">
47 <code className="text-white">
48 {JSON.stringify(data, null, 2)}
49 </code>
50 </pre>
51 ),
52 })
53 }
54
55 return (
56 <div className="flex w-full max-w-md flex-col items-center justify-center space-y-6">
57 <div className="space-y-1 text-center">
58 <h2 className="text-primary text-2xl font-semibold tracking-tight">
59 Contact Us
60 </h2>
61 <p className="text-muted-foreground text-sm">
62 We'd love to hear from you! Fill out the form below.
63 </p>
64 </div>
65
66 <Form {...form}>
67 <form
68 onSubmit={form.handleSubmit(onSubmit)}
69 className="w-full space-y-6"
70 >
71 <FormField
72 control={form.control}
73 name="name"
74 render={({ field }) => (
75 <FormItem>
76 <FormLabel>Name</FormLabel>
77 <FormControl>
78 <Input
79 placeholder="Enter your name"
80 {...field}
81 />
82 </FormControl>
83 <FormDescription>
84 Please enter your full name.
85 </FormDescription>
86 <FormMessage className="text-danger" />
87 </FormItem>
88 )}
89 />
90
91 <FormField
92 control={form.control}
93 name="email"
94 render={({ field }) => (
95 <FormItem>
96 <FormLabel>Email</FormLabel>
97 <FormControl>
98 <Input
99 placeholder="you@example.com"
100 type="email"
101 {...field}
102 />
103 </FormControl>
104 <FormDescription>
105 We'll never share your email with
106 anyone.
107 </FormDescription>
108 <FormMessage className="text-danger" />
109 </FormItem>
110 )}
111 />
112
113 <FormField
114 control={form.control}
115 name="message"
116 render={({ field }) => (
117 <FormItem>
118 <FormLabel>Message</FormLabel>
119 <FormControl>
120 <Textarea
121 placeholder="Write your message here..."
122 rows={4}
123 {...field}
124 />
125 </FormControl>
126 <FormDescription>
127 Tell us a bit about what you need help with.
128 </FormDescription>
129 <FormMessage className="text-danger" />
130 </FormItem>
131 )}
132 />
133
134 <Button type="submit" className="w-full">
135 Send Message
136 </Button>
137 </form>
138 </Form>
139 </div>
140 )
141}
142
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 { z } from 'zod'
7
8import { toast } from '@/components/ui/use-toast'
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({
50 title: 'Settings Saved',
51 description: (
52 <pre className="mt-2 w-[320px] rounded-xl bg-gray-400 p-4">
53 <code className="text-white">
54 {JSON.stringify(data, null, 2)}
55 </code>
56 </pre>
57 ),
58 })
59 }
60
61 const roles = ['admin', 'editor', 'viewer']
62
63 return (
64 <div className="flex w-full max-w-md flex-col items-center justify-center space-y-6">
65 <div className="space-y-1 text-center">
66 <h2 className="text-primary text-2xl font-semibold tracking-tight">
67 Account Settings
68 </h2>
69 <p className="text-muted-foreground text-sm">
70 Manage your username, role, and notification preferences.
71 </p>
72 </div>
73 <Form {...form}>
74 <form
75 onSubmit={form.handleSubmit(onSubmit)}
76 className="w-full space-y-6"
77 >
78 <FormField
79 control={form.control}
80 name="username"
81 render={({ field }) => (
82 <FormItem>
83 <FormLabel>Username</FormLabel>
84 <FormControl>
85 <Input
86 placeholder="Enter your username"
87 {...field}
88 />
89 </FormControl>
90 <FormDescription>
91 This is your public display name.
92 </FormDescription>
93 <FormMessage />
94 </FormItem>
95 )}
96 />
97
98 <FormField
99 control={form.control}
100 name="role"
101 render={({ field }) => (
102 <FormItem>
103 <FormLabel>Role</FormLabel>
104 <FormControl>
105 <DropdownMenu>
106 <DropdownMenuTrigger asChild>
107 <Button
108 variant="outline"
109 className="w-full justify-between"
110 >
111 {field.value || 'Select role'}
112 <ChevronDownIcon className="h-4 w-4 opacity-50" />
113 </Button>
114 </DropdownMenuTrigger>
115 <DropdownMenuContent align="start">
116 {roles.map((r) => (
117 <DropdownMenuItem
118 key={r}
119 onClick={() =>
120 field.onChange(r)
121 }
122 >
123 {r.charAt(0).toUpperCase() +
124 r.slice(1)}
125 </DropdownMenuItem>
126 ))}
127 </DropdownMenuContent>
128 </DropdownMenu>
129 </FormControl>
130 <FormDescription>
131 Choose your permission level.
132 </FormDescription>
133 <FormMessage />
134 </FormItem>
135 )}
136 />
137
138 <FormField
139 control={form.control}
140 name="notification"
141 render={({ field }) => (
142 <FormItem>
143 <FormLabel>Notification Preference</FormLabel>
144 <FormControl>
145 <RadioGroup
146 onValueChange={field.onChange}
147 value={field.value}
148 className="flex flex-row space-x-4"
149 >
150 <div className="flex items-center space-x-2">
151 <RadioGroupItem
152 value="email"
153 id="email"
154 />
155 <FormLabel
156 htmlFor="email"
157 className="font-normal"
158 >
159 Email
160 </FormLabel>
161 </div>
162 <div className="flex items-center space-x-2">
163 <RadioGroupItem
164 value="sms"
165 id="sms"
166 />
167 <FormLabel
168 htmlFor="sms"
169 className="font-normal"
170 >
171 SMS
172 </FormLabel>
173 </div>
174 <div className="flex items-center space-x-2">
175 <RadioGroupItem
176 value="push"
177 id="push"
178 />
179 <FormLabel
180 htmlFor="push"
181 className="font-normal"
182 >
183 Push Notification
184 </FormLabel>
185 </div>
186 </RadioGroup>
187 </FormControl>
188 <FormDescription>
189 How would you like to be notified?
190 </FormDescription>
191 <FormMessage />
192 </FormItem>
193 )}
194 />
195
196 <FormField
197 control={form.control}
198 name="bio"
199 render={({ field }) => (
200 <FormItem>
201 <FormLabel>Bio</FormLabel>
202 <FormControl>
203 <Textarea
204 placeholder="Tell us a little about yourself..."
205 className="w-full rounded-md border bg-transparent p-2 text-sm"
206 rows={4}
207 {...field}
208 />
209 </FormControl>
210 <FormDescription>
211 A short description about you or your work.
212 </FormDescription>
213 <FormMessage />
214 </FormItem>
215 )}
216 />
217
218 <Button type="submit" className="w-full">
219 Save Settings
220 </Button>
221 </form>
222 </Form>
223 </div>
224 )
225}
226
Prop | Type | Default |
---|---|---|
name | string | - |
control | Control | - |
render | ({ field, fieldState }) => ReactNode | - |