Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Build React UIs using shadcn/ui components with Tailwind CSS, Radix primitives, and proper accessibility patterns.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/forms-and-validation.md
1# shadcn/ui - Forms and Validation23## Installation45```bash6npx shadcn@latest add form input textarea select checkbox7npm install react-hook-form @hookform/resolvers zod8```910## Basic Form with Zod Validation1112```tsx13"use client"1415import { zodResolver } from "@hookform/resolvers/zod"16import { useForm } from "react-hook-form"17import * as z from "zod"18import { Button } from "@/components/ui/button"19import {20Form,21FormControl,22FormDescription,23FormField,24FormItem,25FormLabel,26FormMessage,27} from "@/components/ui/form"28import { Input } from "@/components/ui/input"2930const formSchema = z.object({31username: z.string().min(2, { message: "Username must be at least 2 characters." }),32email: z.string().email({ message: "Please enter a valid email address." }),33})3435export function ProfileForm() {36const form = useForm<z.infer<typeof formSchema>>({37resolver: zodResolver(formSchema),38defaultValues: { username: "", email: "" },39})4041function onSubmit(values: z.infer<typeof formSchema>) {42console.log(values)43}4445return (46<Form {...form}>47<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">48<FormField49control={form.control}50name="username"51render={({ field }) => (52<FormItem>53<FormLabel>Username</FormLabel>54<FormControl>55<Input placeholder="shadcn" {...field} />56</FormControl>57<FormDescription>This is your public display name.</FormDescription>58<FormMessage />59</FormItem>60)}61/>62<FormField63control={form.control}64name="email"65render={({ field }) => (66<FormItem>67<FormLabel>Email</FormLabel>68<FormControl>69<Input type="email" placeholder="[email protected]" {...field} />70</FormControl>71<FormMessage />72</FormItem>73)}74/>75<Button type="submit">Submit</Button>76</form>77</Form>78)79}80```8182## Advanced Form with Multiple Field Types8384```tsx85const formSchema = z.object({86username: z.string().min(2).max(50),87email: z.string().email(),88bio: z.string().max(160).min(4),89role: z.enum(["admin", "user", "guest"]),90notifications: z.boolean().default(false),91})9293export function AdvancedForm() {94const form = useForm<z.infer<typeof formSchema>>({95resolver: zodResolver(formSchema),96defaultValues: {97username: "",98email: "",99bio: "",100role: "user",101notifications: false,102},103})104105return (106<Form {...form}>107<form onSubmit={form.handleSubmit(console.log)} className="space-y-8">108{/* Text input */}109<FormField110control={form.control}111name="username"112render={({ field }) => (113<FormItem>114<FormLabel>Username</FormLabel>115<FormControl><Input placeholder="johndoe" {...field} /></FormControl>116<FormMessage />117</FormItem>118)}119/>120121{/* Textarea */}122<FormField123control={form.control}124name="bio"125render={({ field }) => (126<FormItem>127<FormLabel>Bio</FormLabel>128<FormControl>129<Textarea placeholder="Tell us about yourself" className="resize-none" {...field} />130</FormControl>131<FormMessage />132</FormItem>133)}134/>135136{/* Select */}137<FormField138control={form.control}139name="role"140render={({ field }) => (141<FormItem>142<FormLabel>Role</FormLabel>143<Select onValueChange={field.onChange} defaultValue={field.value}>144<FormControl>145<SelectTrigger>146<SelectValue placeholder="Select a role" />147</SelectTrigger>148</FormControl>149<SelectContent>150<SelectItem value="admin">Admin</SelectItem>151<SelectItem value="user">User</SelectItem>152<SelectItem value="guest">Guest</SelectItem>153</SelectContent>154</Select>155<FormMessage />156</FormItem>157)}158/>159160{/* Checkbox */}161<FormField162control={form.control}163name="notifications"164render={({ field }) => (165<FormItem className="flex flex-row items-start space-x-3 space-y-0">166<FormControl>167<Checkbox checked={field.value} onCheckedChange={field.onChange} />168</FormControl>169<div className="space-y-1 leading-none">170<FormLabel>Email notifications</FormLabel>171<FormDescription>Receive emails about your account activity.</FormDescription>172</div>173</FormItem>174)}175/>176177<Button type="submit">Submit</Button>178</form>179</Form>180)181}182```183184## Login Form Pattern (Card + Form)185186```tsx187<Card className="w-[350px]">188<CardHeader>189<CardTitle>Login</CardTitle>190<CardDescription>Enter your credentials to continue</CardDescription>191</CardHeader>192<CardContent>193<Form {...form}>194<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">195<FormField196control={form.control}197name="email"198render={({ field }) => (199<FormItem>200<FormLabel>Email</FormLabel>201<FormControl>202<Input type="email" placeholder="[email protected]" {...field} />203</FormControl>204<FormMessage />205</FormItem>206)}207/>208<FormField209control={form.control}210name="password"211render={({ field }) => (212<FormItem>213<FormLabel>Password</FormLabel>214<FormControl>215<Input type="password" {...field} />216</FormControl>217<FormMessage />218</FormItem>219)}220/>221<Button type="submit" className="w-full">Login</Button>222</form>223</Form>224</CardContent>225</Card>226```227228## Contact Form with API Submission229230```tsx231"use client"232233import { zodResolver } from "@hookform/resolvers/zod"234import { useForm } from "react-hook-form"235import * as z from "zod"236import { Button } from "@/components/ui/button"237import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"238import { Input } from "@/components/ui/input"239import { Textarea } from "@/components/ui/textarea"240import { toast } from "@/components/ui/use-toast"241242const formSchema = z.object({243name: z.string().min(2),244email: z.string().email(),245message: z.string().min(10),246})247248async function onSubmit(values: z.infer<typeof formSchema>) {249try {250const response = await fetch("/api/contact", {251method: "POST",252headers: { "Content-Type": "application/json" },253body: JSON.stringify(values),254})255if (!response.ok) throw new Error("Failed to submit")256toast({ title: "Success!", description: "Your message has been sent." })257} catch {258toast({ variant: "destructive", title: "Error", description: "Failed to send message." })259}260}261262export default function ContactPage() {263const form = useForm<z.infer<typeof formSchema>>({264resolver: zodResolver(formSchema),265})266267return (268<div className="container mx-auto max-w-2xl py-8">269<Form {...form}>270<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">271<FormField control={form.control} name="name"272render={({ field }) => (273<FormItem>274<FormLabel>Name</FormLabel>275<FormControl><Input placeholder="Your name" {...field} /></FormControl>276<FormMessage />277</FormItem>278)}279/>280<FormField control={form.control} name="email"281render={({ field }) => (282<FormItem>283<FormLabel>Email</FormLabel>284<FormControl><Input type="email" placeholder="[email protected]" {...field} /></FormControl>285<FormMessage />286</FormItem>287)}288/>289<FormField control={form.control} name="message"290render={({ field }) => (291<FormItem>292<FormLabel>Message</FormLabel>293<FormControl>294<Textarea placeholder="Your message..." className="resize-none" {...field} />295</FormControl>296<FormMessage />297</FormItem>298)}299/>300<Button type="submit" className="w-full">Send Message</Button>301</form>302</Form>303</div>304)305}306```307308## Route Handler for Form Validation (API)309310```ts311// app/api/contact/route.ts312import { NextRequest, NextResponse } from "next/server"313import { z } from "zod"314315const contactSchema = z.object({316name: z.string().min(2),317email: z.string().email(),318message: z.string().min(10),319})320321export async function POST(request: NextRequest) {322try {323const body = await request.json()324const validated = contactSchema.parse(body)325console.log("Form submission:", validated)326return NextResponse.json({ success: true })327} catch (error) {328if (error instanceof z.ZodError) {329return NextResponse.json({ errors: error.errors }, { status: 400 })330}331return NextResponse.json({ error: "Internal server error" }, { status: 500 })332}333}334```335