Form
Building forms with React Hook Form and Zod.
The Form component provides a structured way to build forms with validation, error handling, and field descriptions. It works seamlessly with React Hook Form and Zod for validation, but can also be used standalone. Built from scratch using React and native HTML form elements.
Code
TypeScript: Copy this code into components/ui/form.tsx:
tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { Label } from "./label"
interface FormContextValue {
errors: Record<string, string[]>
touched: Record<string, boolean>
}
const FormContext = React.createContext<FormContextValue | undefined>(undefined)
const useFormContext = () => {
const context = React.useContext(FormContext)
return context
}
interface FormProps {
onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void
children: React.ReactNode
className?: string
errors?: Record<string, string[]>
touched?: Record<string, boolean>
}
const Form = React.forwardRef<HTMLFormElement, FormProps>(
({ onSubmit, children, className, errors = {}, touched = {}, ...props }, ref) => {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
onSubmit?.(e)
}
return (
<FormContext.Provider value={{ errors, touched }}>
<form
ref={ref}
onSubmit={handleSubmit}
className={cn("space-y-4", className)}
{...props}
>
{children}
</form>
</FormContext.Provider>
)
}
)
Form.displayName = "Form"
interface FormFieldProps {
name: string
children: (props: {
field: {
name: string
value: any
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void
onBlur: (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void
}
error?: string
}) => React.ReactNode
value?: any
onChange?: (value: any) => void
onBlur?: () => void
}
const FormField = ({ name, children, value, onChange, onBlur }: FormFieldProps) => {
const context = useFormContext()
const [localValue, setLocalValue] = React.useState(value ?? "")
const [isTouched, setIsTouched] = React.useState(false)
const fieldValue = value !== undefined ? value : localValue
const error = context?.errors[name]?.[0]
const touched = context?.touched[name] ?? isTouched
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const newValue = e.target.value
if (value === undefined) {
setLocalValue(newValue)
}
onChange?.(newValue)
}
const handleBlur = (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setIsTouched(true)
onBlur?.()
}
return (
<>
{children({
field: {
name,
value: fieldValue,
onChange: handleChange,
onBlur: handleBlur,
},
error: touched ? error : undefined,
})}
</>
)
}
FormField.displayName = "FormField"
interface FormItemProps extends React.HTMLAttributes<HTMLDivElement> {}
const FormItem = React.forwardRef<HTMLDivElement, FormItemProps>(
({ className, ...props }, ref) => {
return (
<div
ref={ref}
className={cn("space-y-2", className)}
{...props}
/>
)
}
)
FormItem.displayName = "FormItem"
interface FormLabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {}
const FormLabel = React.forwardRef<HTMLLabelElement, FormLabelProps>(
({ className, ...props }, ref) => {
return (
<Label
ref={ref}
className={className}
{...props}
/>
)
}
)
FormLabel.displayName = "FormLabel"
interface FormDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {}
const FormDescription = React.forwardRef<HTMLParagraphElement, FormDescriptionProps>(
({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
)
FormDescription.displayName = "FormDescription"
interface FormMessageProps extends React.HTMLAttributes<HTMLParagraphElement> {
children?: React.ReactNode
}
const FormMessage = React.forwardRef<HTMLParagraphElement, FormMessageProps>(
({ className, children, ...props }, ref) => {
if (!children) return null
return (
<p
ref={ref}
className={cn("text-sm font-bold text-destructive", className)}
{...props}
>
{children}
</p>
)
}
)
FormMessage.displayName = "FormMessage"
export {
Form,
FormField,
FormItem,
FormLabel,
FormDescription,
FormMessage,
}JavaScript: Copy this code into components/ui/form.jsx:
jsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { Label } from "./label"
const FormContext = React.createContext(undefined)
const useFormContext = () => {
const context = React.useContext(FormContext)
return context
}
const Form = React.forwardRef(
({ onSubmit, children, className, errors = {}, touched = {}, ...props }, ref) => {
const handleSubmit = (e) => {
e.preventDefault()
onSubmit?.(e)
}
return (
<FormContext.Provider value={{ errors, touched }}>
<form
ref={ref}
onSubmit={handleSubmit}
className={cn("space-y-4", className)}
{...props}
>
{children}
</form>
</FormContext.Provider>
)
}
)
Form.displayName = "Form"
const FormField = ({ name, children, value, onChange, onBlur }) => {
const context = useFormContext()
const [localValue, setLocalValue] = React.useState(value ?? "")
const [isTouched, setIsTouched] = React.useState(false)
const fieldValue = value !== undefined ? value : localValue
const error = context?.errors[name]?.[0]
const touched = context?.touched[name] ?? isTouched
const handleChange = (e) => {
const newValue = e.target.value
if (value === undefined) {
setLocalValue(newValue)
}
onChange?.(newValue)
}
const handleBlur = (e) => {
setIsTouched(true)
onBlur?.()
}
return (
<>
{children({
field: {
name,
value: fieldValue,
onChange: handleChange,
onBlur: handleBlur,
},
error: touched ? error : undefined,
})}
</>
)
}
FormField.displayName = "FormField"
const FormItem = React.forwardRef(
({ className, ...props }, ref) => {
return (
<div
ref={ref}
className={cn("space-y-2", className)}
{...props}
/>
)
}
)
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef(
({ className, ...props }, ref) => {
return (
<Label
ref={ref}
className={className}
{...props}
/>
)
}
)
FormLabel.displayName = "FormLabel"
const FormDescription = React.forwardRef(
({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
)
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef(
({ className, children, ...props }, ref) => {
if (!children) return null
return (
<p
ref={ref}
className={cn("text-sm font-bold text-destructive", className)}
{...props}
>
{children}
</p>
)
}
)
FormMessage.displayName = "FormMessage"
export {
Form,
FormField,
FormItem,
FormLabel,
FormDescription,
FormMessage,
}Usage
TypeScript:
tsx
import {
Form,
FormField,
FormItem,
FormLabel,
FormDescription,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { useState } from "react"
function MyForm() {
const [username, setUsername] = useState("")
const [errors, setErrors] = useState<Record<string, string[]>>({})
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const newErrors: Record<string, string[]> = {}
if (!username || username.length < 3) {
newErrors.username = ["Username must be at least 3 characters"]
}
setErrors(newErrors)
if (Object.keys(newErrors).length === 0) {
}
}
return (
<Form onSubmit={handleSubmit} errors={errors}>
<FormField
name="username"
value={username}
onChange={(value) => setUsername(value as string)}
>
{({ field, error }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<Input {...field} />
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage>{error}</FormMessage>
</FormItem>
)}
</FormField>
<Button type="submit">Submit</Button>
</Form>
)
}JavaScript:
jsx
import {
Form,
FormField,
FormItem,
FormLabel,
FormDescription,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { useState } from "react"
function MyForm() {
const [username, setUsername] = useState("")
const [errors, setErrors] = useState({})
const handleSubmit = (e) => {
e.preventDefault()
const newErrors = {}
if (!username || username.length < 3) {
newErrors.username = ["Username must be at least 3 characters"]
}
setErrors(newErrors)
if (Object.keys(newErrors).length === 0) {
}
}
return (
<Form onSubmit={handleSubmit} errors={errors}>
<FormField
name="username"
value={username}
onChange={(value) => setUsername(value as string)}
>
{({ field, error }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<Input {...field} />
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage>{error}</FormMessage>
</FormItem>
)}
</FormField>
<Button type="submit">Submit</Button>
</Form>
)
}Make sure you also have the lib/utils.ts file with the cn helper function, and the required UI components (input, button,label).